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