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