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,109 @@
1
+ require 'pathname'
2
+
3
+ module Redwood
4
+
5
+ ## meant to be spawned via spawn_modal!
6
+ class FileBrowserMode < LineCursorMode
7
+ RESERVED_ROWS = 1
8
+
9
+ register_keymap do |k|
10
+ k.add :back, "Go back to previous directory", "B"
11
+ k.add :view, "View file", "v"
12
+ k.add :select_file_or_follow_directory, "Select the highlighted file, or follow the directory", :enter
13
+ k.add :reload, "Reload file list", "R"
14
+ end
15
+
16
+ bool_reader :done
17
+ attr_reader :value
18
+
19
+ def initialize dir="."
20
+ @dirs = [Pathname.new(dir).realpath]
21
+ @done = false
22
+ @value = nil
23
+ regen_text
24
+ super :skip_top_rows => RESERVED_ROWS
25
+ end
26
+
27
+ def cwd; @dirs.last end
28
+ def lines; @text.length; end
29
+ def [] i; @text[i]; end
30
+
31
+ protected
32
+
33
+ def back
34
+ return if @dirs.size == 1
35
+ @dirs.pop
36
+ reload
37
+ end
38
+
39
+ def reload
40
+ regen_text
41
+ jump_to_start
42
+ buffer.mark_dirty
43
+ end
44
+
45
+ def view
46
+ name, f = @files[curpos - RESERVED_ROWS]
47
+ return unless f && f.file?
48
+
49
+ begin
50
+ BufferManager.spawn f.to_s, TextMode.new(f.read.ascii)
51
+ rescue SystemCallError => e
52
+ BufferManager.flash e.message
53
+ end
54
+ end
55
+
56
+ def select_file_or_follow_directory
57
+ name, f = @files[curpos - RESERVED_ROWS]
58
+ return unless f
59
+
60
+ if f.directory? && f.to_s != "."
61
+ if f.readable?
62
+ @dirs.push f
63
+ reload
64
+ else
65
+ BufferManager.flash "Permission denied - #{f.realpath}"
66
+ end
67
+ else
68
+ begin
69
+ @value = f.realpath.to_s
70
+ @done = true
71
+ rescue SystemCallError => e
72
+ BufferManager.flash e.message
73
+ end
74
+ end
75
+ end
76
+
77
+ def regen_text
78
+ @files =
79
+ begin
80
+ cwd.entries.sort_by do |f|
81
+ [f.directory? ? 0 : 1, f.basename.to_s]
82
+ end
83
+ rescue SystemCallError => e
84
+ BufferManager.flash "Error: #{e.message}"
85
+ [Pathname.new("."), Pathname.new("..")]
86
+ end.map do |f|
87
+ real_f = cwd + f
88
+ name = f.basename.to_s +
89
+ case
90
+ when real_f.symlink?
91
+ "@"
92
+ when real_f.directory?
93
+ "/"
94
+ else
95
+ ""
96
+ end
97
+ [name, real_f]
98
+ end
99
+
100
+ size_width = @files.max_of { |name, f| f.human_size.length }
101
+ time_width = @files.max_of { |name, f| f.human_time.length }
102
+
103
+ @text = ["#{cwd}:"] + @files.map do |name, f|
104
+ sprintf "%#{time_width}s %#{size_width}s %s", f.human_time, f.human_size, name
105
+ end
106
+ end
107
+ end
108
+
109
+ end
@@ -0,0 +1,82 @@
1
+ module Redwood
2
+
3
+ class ForwardMode < EditMessageMode
4
+ ## TODO: share some of this with reply-mode
5
+ def initialize opts={}
6
+ header = {
7
+ "From" => AccountManager.default_account.full_address,
8
+ }
9
+
10
+ @m = opts[:message]
11
+ header["Subject"] =
12
+ if @m
13
+ "Fwd: " + @m.subj
14
+ elsif opts[:attachments]
15
+ "Fwd: " + opts[:attachments].keys.join(", ")
16
+ end
17
+
18
+ header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
19
+ header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
20
+ header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
21
+
22
+ body =
23
+ if @m
24
+ forward_body_lines @m
25
+ elsif opts[:attachments]
26
+ ["Note: #{opts[:attachments].size.pluralize 'attachment'}."]
27
+ end
28
+
29
+ super :header => header, :body => body, :attachments => opts[:attachments]
30
+ end
31
+
32
+ def self.spawn_nicely opts={}
33
+ to = opts[:to] || (BufferManager.ask_for_contacts(:people, "To: ") or return if ($config[:ask_for_to] != false))
34
+ cc = opts[:cc] || (BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc])
35
+ bcc = opts[:bcc] || (BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc])
36
+
37
+ attachment_hash = {}
38
+ attachments = opts[:attachments] || []
39
+
40
+ if(m = opts[:message])
41
+ m.load_from_source! # read the full message in. you know, maybe i should just make Message#chunks do this....
42
+ attachments += m.chunks.select { |c| c.is_a?(Chunk::Attachment) && !c.quotable? }
43
+ end
44
+
45
+ attachments.each do |c|
46
+ mime_type = MIME::Types[c.content_type].first || MIME::Types["application/octet-stream"].first
47
+ attachment_hash[c.filename] = RMail::Message.make_attachment c.raw_content, mime_type.content_type, mime_type.encoding, c.filename
48
+ end
49
+
50
+ mode = ForwardMode.new :message => opts[:message], :to => to, :cc => cc, :bcc => bcc, :attachments => attachment_hash
51
+
52
+ title = "Forwarding " +
53
+ if opts[:message]
54
+ opts[:message].subj
55
+ elsif attachments
56
+ attachment_hash.keys.join(", ")
57
+ else
58
+ "something"
59
+ end
60
+
61
+ BufferManager.spawn title, mode
62
+ mode.default_edit_message
63
+ end
64
+
65
+ protected
66
+
67
+ def forward_body_lines m
68
+ ["--- Begin forwarded message from #{m.from.mediumname} ---"] +
69
+ m.quotable_header_lines + [""] + m.quotable_body_lines +
70
+ ["--- End forwarded message ---"]
71
+ end
72
+
73
+ def send_message
74
+ return unless super # super returns true if the mail has been sent
75
+ if @m
76
+ @m.add_label :forwarded
77
+ Index.save_message @m
78
+ end
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,19 @@
1
+ module Redwood
2
+
3
+ class HelpMode < TextMode
4
+ def initialize mode, global_keymap
5
+ title = "Help for #{mode.name}"
6
+ super <<EOS
7
+ #{title}
8
+ #{'=' * title.length}
9
+
10
+ #{mode.help_text}
11
+ Global keybindings
12
+ ------------------
13
+ #{global_keymap.help_text}
14
+ EOS
15
+ end
16
+ end
17
+
18
+ end
19
+
@@ -0,0 +1,85 @@
1
+ require "sup/modes/thread_index_mode"
2
+
3
+ module Redwood
4
+
5
+ class InboxMode < ThreadIndexMode
6
+ register_keymap do |k|
7
+ ## overwrite toggle_archived with archive
8
+ k.add :archive, "Archive thread (remove from inbox)", 'a'
9
+ k.add :refine_search, "Refine search", '|'
10
+ end
11
+
12
+ def initialize
13
+ super [:inbox, :sent, :draft], { :label => :inbox, :skip_killed => true }
14
+ raise "can't have more than one!" if defined? @@instance
15
+ @@instance = self
16
+ end
17
+
18
+ def is_relevant? m; (m.labels & [:spam, :deleted, :killed, :inbox]) == Set.new([:inbox]) end
19
+
20
+ def refine_search
21
+ text = BufferManager.ask :search, "refine inbox with query: "
22
+ return unless text && text !~ /^\s*$/
23
+ text = "label:inbox -label:spam -label:deleted " + text
24
+ SearchResultsMode.spawn_from_query text
25
+ end
26
+
27
+ ## label-list-mode wants to be able to raise us if the user selects
28
+ ## the "inbox" label, so we need to keep our singletonness around
29
+ def self.instance; @@instance; end
30
+ def killable?; false; end
31
+
32
+ def archive
33
+ return unless cursor_thread
34
+ thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
35
+
36
+ UndoManager.register "archiving thread" do
37
+ thread.apply_label :inbox
38
+ add_or_unhide thread.first
39
+ Index.save_thread thread
40
+ end
41
+
42
+ cursor_thread.remove_label :inbox
43
+ hide_thread cursor_thread
44
+ regen_text
45
+ Index.save_thread thread
46
+ end
47
+
48
+ def multi_archive threads
49
+ UndoManager.register "archiving #{threads.size.pluralize 'thread'}" do
50
+ threads.map do |t|
51
+ t.apply_label :inbox
52
+ add_or_unhide t.first
53
+ Index.save_thread t
54
+ end
55
+ regen_text
56
+ end
57
+
58
+ threads.each do |t|
59
+ t.remove_label :inbox
60
+ hide_thread t
61
+ end
62
+ regen_text
63
+ threads.each { |t| Index.save_thread t }
64
+ end
65
+
66
+ def handle_unarchived_update sender, m
67
+ add_or_unhide m
68
+ end
69
+
70
+ def handle_archived_update sender, m
71
+ t = thread_containing(m) or return
72
+ hide_thread t
73
+ regen_text
74
+ end
75
+
76
+ def handle_idle_update sender, idle_since
77
+ flush_index
78
+ end
79
+
80
+ def status
81
+ super + " #{Index.size} messages in index"
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,138 @@
1
+ module Redwood
2
+
3
+ class LabelListMode < LineCursorMode
4
+ register_keymap do |k|
5
+ k.add :select_label, "Search by label", :enter
6
+ k.add :reload, "Discard label list and reload", '@'
7
+ k.add :jump_to_next_new, "Jump to next new thread", :tab
8
+ k.add :toggle_show_unread_only, "Toggle between showing all labels and those with unread mail", 'u'
9
+ end
10
+
11
+ HookManager.register "label-list-filter", <<EOS
12
+ Filter the label list, typically to sort.
13
+ Variables:
14
+ counted: an array of counted labels.
15
+ Return value:
16
+ An array of counted labels with sort_by output structure.
17
+ EOS
18
+
19
+ HookManager.register "label-list-format", <<EOS
20
+ Create the sprintf format string for label-list-mode.
21
+ Variables:
22
+ width: the maximum label width
23
+ tmax: the maximum total message count
24
+ umax: the maximum unread message count
25
+ Return value:
26
+ A format string for sprintf
27
+ EOS
28
+
29
+ def initialize
30
+ @labels = []
31
+ @text = []
32
+ @unread_only = false
33
+ super
34
+ UpdateManager.register self
35
+ regen_text
36
+ end
37
+
38
+ def cleanup
39
+ UpdateManager.unregister self
40
+ super
41
+ end
42
+
43
+ def lines; @text.length end
44
+ def [] i; @text[i] end
45
+
46
+ def jump_to_next_new
47
+ n = ((curpos + 1) ... lines).find { |i| @labels[i][1] > 0 } || (0 ... curpos).find { |i| @labels[i][1] > 0 }
48
+ if n
49
+ ## jump there if necessary
50
+ jump_to_line n unless n >= topline && n < botline
51
+ set_cursor_pos n
52
+ else
53
+ BufferManager.flash "No labels messages with unread messages."
54
+ end
55
+ end
56
+
57
+ def focus
58
+ reload # make sure unread message counts are up-to-date
59
+ end
60
+
61
+ def handle_added_update sender, m
62
+ reload
63
+ end
64
+
65
+ protected
66
+
67
+ def toggle_show_unread_only
68
+ @unread_only = !@unread_only
69
+ reload
70
+ end
71
+
72
+ def reload
73
+ regen_text
74
+ buffer.mark_dirty if buffer
75
+ end
76
+
77
+ def regen_text
78
+ @text = []
79
+ labels = LabelManager.all_labels
80
+
81
+ counted = labels.map do |label|
82
+ string = LabelManager.string_for label
83
+ total = Index.num_results_for :label => label
84
+ unread = (label == :unread)? total : Index.num_results_for(:labels => [label, :unread])
85
+ [label, string, total, unread]
86
+ end
87
+
88
+ if HookManager.enabled? "label-list-filter"
89
+ counts = HookManager.run "label-list-filter", :counted => counted
90
+ else
91
+ counts = counted.sort_by { |l, s, t, u| s.downcase }
92
+ end
93
+
94
+ width = counts.max_of { |l, s, t, u| s.length }
95
+ tmax = counts.max_of { |l, s, t, u| t }
96
+ umax = counts.max_of { |l, s, t, u| u }
97
+
98
+ if @unread_only
99
+ counts.delete_if { | l, s, t, u | u == 0 }
100
+ end
101
+
102
+ @labels = []
103
+ counts.map do |label, string, total, unread|
104
+ ## if we've done a search and there are no messages for this label, we can delete it from the
105
+ ## list. BUT if it's a brand-new label, the user may not have sync'ed it to the index yet, so
106
+ ## don't delete it in this case.
107
+ ##
108
+ ## this is all a hack. what should happen is:
109
+ ## TODO make the labelmanager responsible for label counts
110
+ ## and then it can listen to labeled and unlabeled events, etc.
111
+ if total == 0 && !LabelManager::RESERVED_LABELS.include?(label) && !LabelManager.new_label?(label)
112
+ debug "no hits for label #{label}, deleting"
113
+ LabelManager.delete label
114
+ next
115
+ end
116
+
117
+ fmt = HookManager.run "label-list-format", :width => width, :tmax => tmax, :umax => umax
118
+ if !fmt
119
+ fmt = "%#{width + 1}s %5d %s, %5d unread"
120
+ end
121
+
122
+ @text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
123
+ sprintf(fmt, string, total, total == 1 ? " message" : "messages", unread)]]
124
+ @labels << [label, unread]
125
+ yield i if block_given?
126
+ end.compact
127
+
128
+ BufferManager.flash "No labels with unread messages!" if counts.empty? && @unread_only
129
+ end
130
+
131
+ def select_label
132
+ label, num_unread = @labels[curpos]
133
+ return unless label
134
+ LabelSearchResultsMode.spawn_nicely label
135
+ end
136
+ end
137
+
138
+ end
@@ -0,0 +1,38 @@
1
+ module Redwood
2
+
3
+ class LabelSearchResultsMode < ThreadIndexMode
4
+ def initialize labels
5
+ @labels = labels
6
+ opts = { :labels => @labels }
7
+ opts[:load_deleted] = true if labels.include? :deleted
8
+ opts[:load_spam] = true if labels.include? :spam
9
+ super [], opts
10
+ end
11
+
12
+ register_keymap do |k|
13
+ k.add :refine_search, "Refine search", '|'
14
+ end
15
+
16
+ def refine_search
17
+ label_query = @labels.size > 1 ? "(#{@labels.join('||')})" : @labels.first
18
+ query = BufferManager.ask :search, "refine query: ", "+label:#{label_query} "
19
+ return unless query && query !~ /^\s*$/
20
+ SearchResultsMode.spawn_from_query query
21
+ end
22
+
23
+ def is_relevant? m; @labels.all? { |l| m.has_label? l } end
24
+
25
+ def self.spawn_nicely label
26
+ label = LabelManager.label_for(label) unless label.is_a?(Symbol)
27
+ case label
28
+ when nil
29
+ when :inbox
30
+ BufferManager.raise_to_front InboxMode.instance.buffer
31
+ else
32
+ b, new = BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] }
33
+ b.mode.load_threads :num => b.content_height if new
34
+ end
35
+ end
36
+ end
37
+
38
+ end