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
@@ -0,0 +1,328 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'tempfile'
4
+ require 'rbconfig'
5
+ require 'shellwords'
6
+
7
+ ## Here we define all the "chunks" that a message is parsed
8
+ ## into. Chunks are used by ThreadViewMode to render a message. Chunks
9
+ ## are used for both MIME stuff like attachments, for Sup's parsing of
10
+ ## the message body into text, quote, and signature regions, and for
11
+ ## notices like "this message was decrypted" or "this message contains
12
+ ## a valid signature"---basically, anything we want to differentiate
13
+ ## at display time.
14
+ ##
15
+ ## A chunk can be inlineable, expandable, or viewable. If it's
16
+ ## inlineable, #color and #lines are called and the output is treated
17
+ ## as part of the message text. This is how Text and one-line Quotes
18
+ ## and Signatures work.
19
+ ##
20
+ ## If it's not inlineable but is expandable, #patina_color and
21
+ ## #patina_text are called to generate a "patina" (a one-line widget,
22
+ ## basically), and the user can press enter to toggle the display of
23
+ ## the chunk content, which is generated from #color and #lines as
24
+ ## above. This is how Quote, Signature, and most widgets
25
+ ## work. Exandable chunks can additionally define #initial_state to be
26
+ ## :open if they want to start expanded (default is to start collapsed).
27
+ ##
28
+ ## If it's not expandable but is viewable, a patina is displayed using
29
+ ## #patina_color and #patina_text, but no toggling is allowed. Instead,
30
+ ## if #view! is defined, pressing enter on the widget calls view! and
31
+ ## (if that returns false) #to_s. Otherwise, enter does nothing. This
32
+ ## is how non-inlineable attachments work.
33
+ ##
34
+ ## Independent of all that, a chunk can be quotable, in which case it's
35
+ ## included as quoted text during a reply. Text, Quotes, and mime-parsed
36
+ ## attachments are quotable; Signatures are not.
37
+
38
+ ## monkey-patch time: make temp files have the right extension
39
+ ## Backport from Ruby 1.9.2 for versions lower than 1.8.7
40
+ if RUBY_VERSION < '1.8.7'
41
+ class Tempfile
42
+ def make_tmpname(prefix_suffix, n)
43
+ case prefix_suffix
44
+ when String
45
+ prefix = prefix_suffix
46
+ suffix = ""
47
+ when Array
48
+ prefix = prefix_suffix[0]
49
+ suffix = prefix_suffix[1]
50
+ else
51
+ raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
52
+ end
53
+ t = Time.now.strftime("%Y%m%d")
54
+ path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
55
+ path << "-#{n}" if n
56
+ path << suffix
57
+ end
58
+ end
59
+ end
60
+
61
+
62
+ module Redwood
63
+ module Chunk
64
+ class Attachment
65
+ HookManager.register "mime-decode", <<EOS
66
+ Decodes a MIME attachment into text form. The text will be displayed
67
+ directly in Sup. For attachments that you wish to use a separate program
68
+ to view (e.g. images), you should use the mime-view hook instead.
69
+
70
+ Variables:
71
+ content_type: the content-type of the attachment
72
+ charset: the charset of the attachment, if applicable
73
+ filename: the filename of the attachment as saved to disk
74
+ sibling_types: if this attachment is part of a multipart MIME attachment,
75
+ an array of content-types for all attachments. Otherwise,
76
+ the empty array.
77
+ Return value:
78
+ The decoded text of the attachment, or nil if not decoded.
79
+ EOS
80
+
81
+
82
+ HookManager.register "mime-view", <<EOS
83
+ Views a non-text MIME attachment. This hook allows you to run
84
+ third-party programs for attachments that require such a thing (e.g.
85
+ images). To instead display a text version of the attachment directly in
86
+ Sup, use the mime-decode hook instead.
87
+
88
+ Note that by default (at least on systems that have a run-mailcap command),
89
+ Sup uses the default mailcap handler for the attachment's MIME type. If
90
+ you want a particular behavior to be global, you may wish to change your
91
+ mailcap instead.
92
+
93
+ Variables:
94
+ content_type: the content-type of the attachment
95
+ filename: the filename of the attachment as saved to disk
96
+ Return value:
97
+ True if the viewing was successful, false otherwise. If false, calling
98
+ /usr/bin/run-mailcap will be tried.
99
+ EOS
100
+ #' stupid ruby-mode
101
+
102
+ ## raw_content is the post-MIME-decode content. this is used for
103
+ ## saving the attachment to disk.
104
+ attr_reader :content_type, :filename, :lines, :raw_content
105
+ bool_reader :quotable
106
+
107
+ ## store tempfile objects as class variables so that they
108
+ ## are not removed when the viewing process returns. they
109
+ ## should be garbage collected when the class variable is removed.
110
+ @@view_tempfiles = []
111
+
112
+ def initialize content_type, filename, encoded_content, sibling_types
113
+ @content_type = content_type.downcase
114
+ if Shellwords.escape(@content_type) != @content_type
115
+ warn "content_type #{@content_type} is not safe, changed to application/octet-stream"
116
+ @content_type = 'application/octet-stream'
117
+ end
118
+
119
+ @filename = filename
120
+ @quotable = false # changed to true if we can parse it through the
121
+ # mime-decode hook, or if it's plain text
122
+ @raw_content =
123
+ if encoded_content.body
124
+ encoded_content.decode
125
+ else
126
+ "For some bizarre reason, RubyMail was unable to parse this attachment.\n"
127
+ end
128
+
129
+ text = case @content_type
130
+ when /^text\/plain\b/
131
+ @raw_content
132
+ else
133
+ HookManager.run "mime-decode", :content_type => @content_type,
134
+ :filename => lambda { write_to_disk },
135
+ :charset => encoded_content.charset,
136
+ :sibling_types => sibling_types
137
+ end
138
+
139
+ @lines = nil
140
+ if text
141
+ text = text.transcode(encoded_content.charset || $encoding, text.encoding)
142
+ begin
143
+ @lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
144
+ rescue Encoding::CompatibilityError
145
+ @lines = text.fix_encoding!.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
146
+ debug "error while decoding message text, falling back to default encoding, expect errors in encoding: #{text.fix_encoding!}"
147
+ end
148
+
149
+ @quotable = true
150
+ end
151
+ end
152
+
153
+ def color; :text_color end
154
+ def patina_color; :attachment_color end
155
+ def patina_text
156
+ if expandable?
157
+ "Attachment: #{filename} (#{lines.length} lines)"
158
+ else
159
+ "Attachment: #{filename} (#{content_type}; #{@raw_content.size.to_human_size})"
160
+ end
161
+ end
162
+
163
+ ## an attachment is exapndable if we've managed to decode it into
164
+ ## something we can display inline. otherwise, it's viewable.
165
+ def inlineable?; false end
166
+ def expandable?; !viewable? end
167
+ def initial_state; :open end
168
+ def viewable?; @lines.nil? end
169
+ def view_default! path
170
+ case RbConfig::CONFIG['arch']
171
+ when /darwin/
172
+ cmd = "open #{path}"
173
+ else
174
+ cmd = "/usr/bin/run-mailcap --action=view #{@content_type}:#{path}"
175
+ end
176
+ debug "running: #{cmd.inspect}"
177
+ BufferManager.shell_out(cmd)
178
+ $? == 0
179
+ end
180
+
181
+ def view!
182
+ write_to_disk do |path|
183
+ ret = HookManager.run "mime-view", :content_type => @content_type,
184
+ :filename => path
185
+ ret || view_default!(path)
186
+ end
187
+ end
188
+
189
+ def write_to_disk
190
+ begin
191
+ # Add the original extension to the generated tempfile name only if the
192
+ # extension is "safe" (won't be interpreted by the shell). Since
193
+ # Tempfile.new always generates safe file names this should prevent
194
+ # attacking the user with funny attachment file names.
195
+ tempname = if (File.extname @filename) =~ /^\.[[:alnum:]]+$/ then
196
+ ["sup-attachment", File.extname(@filename)]
197
+ else
198
+ "sup-attachment"
199
+ end
200
+
201
+ file = Tempfile.new(tempname)
202
+ file.print @raw_content
203
+ file.flush
204
+
205
+ @@view_tempfiles.push file # make sure the tempfile is not garbage collected before sup stops
206
+
207
+ yield file.path if block_given?
208
+ return file.path
209
+ ensure
210
+ file.close
211
+ end
212
+ end
213
+
214
+ ## used when viewing the attachment as text
215
+ def to_s
216
+ @lines || @raw_content
217
+ end
218
+ end
219
+
220
+ class Text
221
+
222
+ attr_reader :lines
223
+ def initialize lines
224
+ @lines = lines
225
+ ## trim off all empty lines except one
226
+ @lines.pop while @lines.length > 1 && @lines[-1] =~ /^\s*$/ && @lines[-2] =~ /^\s*$/
227
+ end
228
+
229
+ def inlineable?; true end
230
+ def quotable?; true end
231
+ def expandable?; false end
232
+ def viewable?; false end
233
+ def color; :text_color end
234
+ end
235
+
236
+ class Quote
237
+ attr_reader :lines
238
+ def initialize lines
239
+ @lines = lines
240
+ end
241
+
242
+ def inlineable?; @lines.length == 1 end
243
+ def quotable?; true end
244
+ def expandable?; !inlineable? end
245
+ def viewable?; false end
246
+
247
+ def patina_color; :quote_patina_color end
248
+ def patina_text; "(#{lines.length} quoted lines)" end
249
+ def color; :quote_color end
250
+ end
251
+
252
+ class Signature
253
+ attr_reader :lines
254
+ def initialize lines
255
+ @lines = lines
256
+ end
257
+
258
+ def inlineable?; @lines.length == 1 end
259
+ def quotable?; false end
260
+ def expandable?; !inlineable? end
261
+ def viewable?; false end
262
+
263
+ def patina_color; :sig_patina_color end
264
+ def patina_text; "(#{lines.length}-line signature)" end
265
+ def color; :sig_color end
266
+ end
267
+
268
+ class EnclosedMessage
269
+ attr_reader :lines
270
+ def initialize from, to, cc, date, subj
271
+ @from = from ? "unknown sender" : from.full_address
272
+ @to = to ? "" : to.map { |p| p.full_address }.join(", ")
273
+ @cc = cc ? "" : cc.map { |p| p.full_address }.join(", ")
274
+ if date
275
+ @date = date.rfc822
276
+ else
277
+ @date = ""
278
+ end
279
+
280
+ @subj = subj
281
+
282
+ @lines = "\nFrom: #{from}\n"
283
+ @lines += "To: #{to}\n"
284
+ if !cc.empty?
285
+ @lines += "Cc: #{cc}\n"
286
+ end
287
+ @lines += "Date: #{date}\n"
288
+ @lines += "Subject: #{subj}\n\n"
289
+ end
290
+
291
+ def inlineable?; false end
292
+ def quotable?; false end
293
+ def expandable?; true end
294
+ def initial_state; :closed end
295
+ def viewable?; false end
296
+
297
+ def patina_color; :generic_notice_patina_color end
298
+ def patina_text; "Begin enclosed message sent on #{@date}" end
299
+
300
+ def color; :quote_color end
301
+ end
302
+
303
+ class CryptoNotice
304
+ attr_reader :lines, :status, :patina_text
305
+
306
+ def initialize status, description, lines=[]
307
+ @status = status
308
+ @patina_text = description
309
+ @lines = lines
310
+ end
311
+
312
+ def patina_color
313
+ case status
314
+ when :valid then :cryptosig_valid_color
315
+ when :valid_untrusted then :cryptosig_valid_untrusted_color
316
+ when :invalid then :cryptosig_invalid_color
317
+ else :cryptosig_unknown_color
318
+ end
319
+ end
320
+ def color; patina_color end
321
+
322
+ def inlineable?; false end
323
+ def quotable?; false end
324
+ def expandable?; !@lines.empty? end
325
+ def viewable?; false end
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,140 @@
1
+ require 'open3'
2
+ module Redwood
3
+
4
+ class Mode
5
+ attr_accessor :buffer
6
+ @@keymaps = {}
7
+
8
+ def self.register_keymap keymap=nil, &b
9
+ keymap = Keymap.new(&b) if keymap.nil?
10
+ @@keymaps[self] = keymap
11
+ end
12
+
13
+ def self.keymap
14
+ @@keymaps[self] || register_keymap
15
+ end
16
+
17
+ def self.keymaps
18
+ @@keymaps
19
+ end
20
+
21
+ def initialize
22
+ @buffer = nil
23
+ end
24
+
25
+ def self.make_name s; s.gsub(/.*::/, "").camel_to_hyphy; end
26
+ def name; Mode.make_name self.class.name; end
27
+
28
+ def self.load_all_modes dir
29
+ Dir[File.join(dir, "*.rb")].each do |f|
30
+ $stderr.puts "## loading mode #{f}"
31
+ require f
32
+ end
33
+ end
34
+
35
+ def killable?; true; end
36
+ def unsaved?; false end
37
+ def draw; end
38
+ def focus; end
39
+ def blur; end
40
+ def cancel_search!; end
41
+ def in_search?; false end
42
+ def status; ""; end
43
+ def resize rows, cols; end
44
+ def cleanup
45
+ @buffer = nil
46
+ end
47
+
48
+ def resolve_input c
49
+ ancestors.each do |klass| # try all keymaps in order of ancestry
50
+ next unless @@keymaps.member?(klass)
51
+ action = BufferManager.resolve_input_with_keymap c, @@keymaps[klass]
52
+ return action if action
53
+ end
54
+ nil
55
+ end
56
+
57
+ def handle_input c
58
+ action = resolve_input(c) or return false
59
+ send action
60
+ true
61
+ end
62
+
63
+ def help_text
64
+ used_keys = {}
65
+ ancestors.map do |klass|
66
+ km = @@keymaps[klass] or next
67
+ title = "Keybindings from #{Mode.make_name klass.name}"
68
+ s = <<EOS
69
+ #{title}
70
+ #{'-' * title.display_length}
71
+
72
+ #{km.help_text used_keys}
73
+ EOS
74
+ begin
75
+ used_keys.merge! km.keysyms.to_boolean_h
76
+ rescue ArgumentError
77
+ raise km.keysyms.inspect
78
+ end
79
+ s
80
+ end.compact.join "\n"
81
+ end
82
+
83
+ ### helper functions
84
+
85
+ def save_to_file fn, talk=true
86
+ if File.exists? fn
87
+ unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
88
+ info "Not overwriting #{fn}"
89
+ return
90
+ end
91
+ end
92
+ begin
93
+ File.open(fn, "w") { |f| yield f }
94
+ BufferManager.flash "Successfully wrote #{fn}." if talk
95
+ true
96
+ rescue SystemCallError, IOError => e
97
+ m = "Error writing file: #{e.message}"
98
+ info m
99
+ BufferManager.flash m
100
+ false
101
+ end
102
+ end
103
+
104
+ def pipe_to_process command
105
+ Open3.popen3(command) do |input, output, error|
106
+ err, data, * = IO.select [error], [input], nil
107
+
108
+ unless err.empty?
109
+ message = err.first.read
110
+ if message =~ /^\s*$/
111
+ warn "error running #{command} (but no error message)"
112
+ BufferManager.flash "Error running #{command}!"
113
+ else
114
+ warn "error running #{command}: #{message}"
115
+ BufferManager.flash "Error: #{message}"
116
+ end
117
+ return
118
+ end
119
+
120
+ data = data.first
121
+ data.sync = false # buffer input
122
+
123
+ yield data
124
+ data.close # output will block unless input is closed
125
+
126
+ ## BUG?: shows errors or output but not both....
127
+ data, * = IO.select [output, error], nil, nil
128
+ data = data.first
129
+
130
+ if data.eof
131
+ BufferManager.flash "'#{command}' done!"
132
+ nil
133
+ else
134
+ data.read
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ end