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,110 @@
1
+ # encoding: utf-8
2
+
3
+ module Redwood
4
+
5
+ class SearchManager
6
+ include Redwood::Singleton
7
+
8
+ class ExpansionError < StandardError; end
9
+
10
+ attr_reader :predefined_searches
11
+
12
+ def initialize fn
13
+ @fn = fn
14
+ @searches = {}
15
+ if File.exists? fn
16
+ IO.foreach(fn) do |l|
17
+ l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
18
+ @searches[$1] = $2
19
+ end
20
+ end
21
+ @modified = false
22
+
23
+ @predefined_searches = { 'All mail' => 'Search all mail.' }
24
+ @predefined_queries = { 'All mail'.to_sym => { :qobj => Xapian::Query.new('Kmail'),
25
+ :load_spam => false,
26
+ :load_deleted => false,
27
+ :load_killed => false,
28
+ :text => 'Search all mail.'}
29
+ }
30
+ @predefined_searches.each do |k,v|
31
+ @searches[k] = v
32
+ end
33
+ end
34
+
35
+ def predefined_queries; return @predefined_queries; end
36
+ def all_searches; return @searches.keys.sort; end
37
+ def search_string_for name;
38
+ if @predefined_searches.keys.member? name
39
+ return name.to_sym
40
+ end
41
+ return @searches[name];
42
+ end
43
+ def valid_name? name; name =~ /^[\w-]+$/; end
44
+ def name_format_hint; "letters, numbers, underscores and dashes only"; end
45
+
46
+ def add name, search_string
47
+ return unless valid_name? name
48
+ if @predefined_searches.has_key? name
49
+ warn "cannot add search: #{name} is already taken by a predefined search"
50
+ return
51
+ end
52
+ @searches[name] = search_string
53
+ @modified = true
54
+ end
55
+
56
+ def rename old, new
57
+ return unless @searches.has_key? old
58
+ if [old, new].any? { |x| @predefined_searches.has_key? x }
59
+ warn "cannot rename search: #{old} or #{new} is already taken by a predefined search"
60
+ return
61
+ end
62
+ search_string = @searches[old]
63
+ delete old if add new, search_string
64
+ end
65
+
66
+ def edit name, search_string
67
+ return unless @searches.has_key? name
68
+ if @predefined_searches.has_key? name
69
+ warn "cannot edit predefined search: #{name}."
70
+ return
71
+ end
72
+ @searches[name] = search_string
73
+ @modified = true
74
+ end
75
+
76
+ def delete name
77
+ return unless @searches.has_key? name
78
+ if @predefined_searches.has_key? name
79
+ warn "cannot delete predefined search: #{name}."
80
+ return
81
+ end
82
+ @searches.delete name
83
+ @modified = true
84
+ end
85
+
86
+ def expand search_string
87
+ expanded = search_string.dup
88
+ until (matches = expanded.scan(/\{([\w-]+)\}/).flatten).empty?
89
+ if !(unknown = matches - @searches.keys).empty?
90
+ error_message = "Unknown \"#{unknown.join('", "')}\" when expanding \"#{search_string}\""
91
+ elsif expanded.size >= 2048
92
+ error_message = "Check for infinite recursion in \"#{search_string}\""
93
+ end
94
+ if error_message
95
+ warn error_message
96
+ raise ExpansionError, error_message
97
+ end
98
+ matches.each { |n| expanded.gsub! "{#{n}}", "(#{@searches[n]})" if @searches.has_key? n }
99
+ end
100
+ return expanded
101
+ end
102
+
103
+ def save
104
+ return unless @modified
105
+ File.open(@fn, "w:UTF-8") { |f| (@searches - @predefined_searches.keys).sort.each { |(n, s)| f.puts "#{n}: #{s}" } }
106
+ @modified = false
107
+ end
108
+ end
109
+
110
+ end
@@ -0,0 +1,58 @@
1
+ module Redwood
2
+
3
+ class SentManager
4
+ include Redwood::Singleton
5
+
6
+ attr_reader :source, :source_uri
7
+
8
+ def initialize source_uri
9
+ @source = nil
10
+ @source_uri = source_uri
11
+ end
12
+
13
+ def source_id; @source.id; end
14
+
15
+ def source= s
16
+ raise FatalSourceError.new("Configured sent_source [#{s.uri}] can't store mail. Correct your configuration.") unless s.respond_to? :store_message
17
+ @source_uri = s.uri
18
+ @source = s
19
+ end
20
+
21
+ def default_source
22
+ @source = SentLoader.new
23
+ @source_uri = @source.uri
24
+ @source
25
+ end
26
+
27
+ def write_sent_message date, from_email, &block
28
+ ::Thread.new do
29
+ debug "store the sent message (locking sent source..)"
30
+ @source.synchronize do
31
+ @source.store_message date, from_email, &block
32
+ end
33
+ PollManager.poll_from @source
34
+ end
35
+ end
36
+ end
37
+
38
+ class SentLoader < MBox
39
+ yaml_properties
40
+
41
+ def initialize
42
+ @filename = Redwood::SENT_FN
43
+ File.open(@filename, "w") { } unless File.exists? @filename
44
+ super "mbox://" + @filename, true, $config[:archive_sent]
45
+ end
46
+
47
+ def file_path; @filename end
48
+
49
+ def to_s; 'sup://sent'; end
50
+ def uri; 'sup://sent' end
51
+
52
+ def id; 9998; end
53
+ def labels; [:inbox, :sent]; end
54
+ def default_labels; []; end
55
+ def read?; true; end
56
+ end
57
+
58
+ end
@@ -0,0 +1,45 @@
1
+ require "sup/index"
2
+
3
+ module Redwood
4
+ # Provides label tweaking service to the user.
5
+ # Working as the backend of ConsoleMode.
6
+ #
7
+ # Should become the backend of bin/sup-tweak-labels in the future.
8
+ class LabelService
9
+ # @param index [Redwood::Index]
10
+ def initialize index=Index.instance
11
+ @index = index
12
+ end
13
+
14
+ def add_labels query, *labels
15
+ run_on_each_message(query) do |m|
16
+ labels.each {|l| m.add_label l }
17
+ end
18
+ end
19
+
20
+ def remove_labels query, *labels
21
+ run_on_each_message(query) do |m|
22
+ labels.each {|l| m.remove_label l }
23
+ end
24
+ end
25
+
26
+
27
+ private
28
+ def run_on_each_message query, &operation
29
+ count = 0
30
+
31
+ find_messages(query).each do |m|
32
+ operation.call(m)
33
+ @index.update_message_state m
34
+ count += 1
35
+ end
36
+
37
+ @index.save_index
38
+ count
39
+ end
40
+
41
+ def find_messages query
42
+ @index.find_messages(query)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,244 @@
1
+ require "sup/rfc2047"
2
+ require "monitor"
3
+
4
+ module Redwood
5
+
6
+ class SourceError < StandardError
7
+ def initialize *a
8
+ raise "don't instantiate me!" if SourceError.is_a?(self.class)
9
+ super
10
+ end
11
+ end
12
+ class OutOfSyncSourceError < SourceError; end
13
+ class FatalSourceError < SourceError; end
14
+
15
+ class Source
16
+ ## Implementing a new source should be easy, because Sup only needs
17
+ ## to be able to:
18
+ ## 1. See how many messages it contains
19
+ ## 2. Get an arbitrary message
20
+ ## 3. (optional) see whether the source has marked it read or not
21
+ ##
22
+ ## In particular, Sup doesn't need to move messages, mark them as
23
+ ## read, delete them, or anything else. (Well, it's nice to be able
24
+ ## to delete them, but that is optional.)
25
+ ##
26
+ ## Messages are identified internally based on the message id, and stored
27
+ ## with an unique document id. Along with the message, source information
28
+ ## that can contain arbitrary fields (set up by the source) is stored. This
29
+ ## information will be passed back to the source when a message in the
30
+ ## index (Sup database) needs to be identified to its source, e.g. when
31
+ ## re-reading or modifying a unique message.
32
+ ##
33
+ ## To write a new source, subclass this class, and implement:
34
+ ##
35
+ ## - initialize
36
+ ## - load_header offset
37
+ ## - load_message offset
38
+ ## - raw_header offset
39
+ ## - raw_message offset
40
+ ## - store_message (optional)
41
+ ## - poll (loads new messages)
42
+ ## - go_idle (optional)
43
+ ##
44
+ ## All exceptions relating to accessing the source must be caught
45
+ ## and rethrown as FatalSourceErrors or OutOfSyncSourceErrors.
46
+ ## OutOfSyncSourceErrors should be used for problems that a call to
47
+ ## sup-sync will fix (namely someone's been playing with the source
48
+ ## from another client); FatalSourceErrors can be used for anything
49
+ ## else (e.g. the imap server is down or the maildir is missing.)
50
+ ##
51
+ ## Finally, be sure the source is thread-safe, since it WILL be
52
+ ## pummelled from multiple threads at once.
53
+ ##
54
+ ## Examples for you to look at: mbox.rb and maildir.rb.
55
+
56
+ bool_accessor :usual, :archived
57
+ attr_reader :uri, :usual
58
+ attr_accessor :id
59
+
60
+ def initialize uri, usual=true, archived=false, id=nil
61
+ raise ArgumentError, "id must be an integer: #{id.inspect}" unless id.is_a? Fixnum if id
62
+
63
+ @uri = uri
64
+ @usual = usual
65
+ @archived = archived
66
+ @id = id
67
+
68
+ @poll_lock = Monitor.new
69
+ end
70
+
71
+ ## overwrite me if you have a disk incarnation
72
+ def file_path; nil end
73
+
74
+ def to_s; @uri.to_s; end
75
+ def == o; o.uri == uri; end
76
+ def is_source_for? uri; uri == @uri; end
77
+
78
+ def read?; false; end
79
+
80
+ ## release resources that are easy to reacquire. it is called
81
+ ## after processing a source (e.g. polling) to prevent resource
82
+ ## leaks (esp. file descriptors).
83
+ def go_idle; end
84
+
85
+ ## Returns an array containing all the labels that are natively
86
+ ## supported by this source
87
+ def supported_labels?; [] end
88
+
89
+ ## Returns an array containing all the labels that are currently in
90
+ ## the location filename
91
+ def labels? info; [] end
92
+
93
+ ## Yields values of the form [Symbol, Hash]
94
+ ## add: info, labels, progress
95
+ ## delete: info, progress
96
+ def poll
97
+ unimplemented
98
+ end
99
+
100
+ def valid? info
101
+ true
102
+ end
103
+
104
+ def synchronize &block
105
+ @poll_lock.synchronize &block
106
+ end
107
+
108
+ def try_lock
109
+ acquired = @poll_lock.try_enter
110
+ if acquired
111
+ debug "lock acquired for: #{self}"
112
+ else
113
+ debug "could not acquire lock for: #{self}"
114
+ end
115
+ acquired
116
+ end
117
+
118
+ def unlock
119
+ @poll_lock.exit
120
+ debug "lock released for: #{self}"
121
+ end
122
+
123
+ ## utility method to read a raw email header from an IO stream and turn it
124
+ ## into a hash of key-value pairs. minor special semantics for certain headers.
125
+ ##
126
+ ## THIS IS A SPEED-CRITICAL SECTION. Everything you do here will have a
127
+ ## significant effect on Sup's processing speed of email from ALL sources.
128
+ ## Little things like string interpolation, regexp interpolation, += vs <<,
129
+ ## all have DRAMATIC effects. BE CAREFUL WHAT YOU DO!
130
+ def self.parse_raw_email_header f
131
+ header = {}
132
+ last = nil
133
+
134
+ while(line = f.gets)
135
+ case line
136
+ ## these three can occur multiple times, and we want the first one
137
+ when /^(Delivered-To|X-Original-To|Envelope-To):\s*(.*?)\s*$/i; header[last = $1.downcase] ||= $2
138
+ ## regular header: overwrite (not that we should see more than one)
139
+ ## TODO: figure out whether just using the first occurrence changes
140
+ ## anything (which would simplify the logic slightly)
141
+ when /^([^:\s]+):\s*(.*?)\s*$/i; header[last = $1.downcase] = $2
142
+ when /^\r*$/; break # blank line signifies end of header
143
+ else
144
+ if last
145
+ header[last] << " " unless header[last].empty?
146
+ header[last] << line.strip
147
+ end
148
+ end
149
+ end
150
+
151
+ %w(subject from to cc bcc).each do |k|
152
+ v = header[k] or next
153
+ next unless Rfc2047.is_encoded? v
154
+ header[k] = begin
155
+ Rfc2047.decode_to $encoding, v
156
+ rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
157
+ #debug "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
158
+ v
159
+ end
160
+ end
161
+ header
162
+ end
163
+
164
+ protected
165
+
166
+ ## convenience function
167
+ def parse_raw_email_header f; self.class.parse_raw_email_header f end
168
+
169
+ def Source.expand_filesystem_uri uri
170
+ uri.gsub "~", File.expand_path("~")
171
+ end
172
+ end
173
+
174
+ ## if you have a @labels instance variable, include this
175
+ ## to serialize them nicely as an array, rather than as a
176
+ ## nasty set.
177
+ module SerializeLabelsNicely
178
+ def before_marshal # can return an object
179
+ c = clone
180
+ c.instance_eval { @labels = (@labels.to_a.map { |l| l.to_s }).sort }
181
+ c
182
+ end
183
+
184
+ def after_unmarshal!
185
+ @labels = Set.new(@labels.to_a.map { |s| s.to_sym })
186
+ end
187
+ end
188
+
189
+ class SourceManager
190
+ include Redwood::Singleton
191
+
192
+ def initialize
193
+ @sources = {}
194
+ @sources_dirty = false
195
+ @source_mutex = Monitor.new
196
+ end
197
+
198
+ def [](id)
199
+ @source_mutex.synchronize { @sources[id] }
200
+ end
201
+
202
+ def add_source source
203
+ @source_mutex.synchronize do
204
+ raise "duplicate source!" if @sources.include? source
205
+ @sources_dirty = true
206
+ max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
207
+ source.id ||= (max || 0) + 1
208
+ ##source.id += 1 while @sources.member? source.id
209
+ @sources[source.id] = source
210
+ end
211
+ end
212
+
213
+ def sources
214
+ ## favour the inbox by listing non-archived sources first
215
+ @source_mutex.synchronize { @sources.values }.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
216
+ end
217
+
218
+ def source_for uri
219
+ expanded_uri = Source.expand_filesystem_uri(uri)
220
+ sources.find { |s| s.is_source_for? expanded_uri }
221
+ end
222
+
223
+ def usual_sources; sources.find_all { |s| s.usual? }; end
224
+ def unusual_sources; sources.find_all { |s| !s.usual? }; end
225
+
226
+ def load_sources fn=Redwood::SOURCE_FN
227
+ source_array = Redwood::load_yaml_obj(fn) || []
228
+ @source_mutex.synchronize do
229
+ @sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
230
+ @sources_dirty = false
231
+ end
232
+ end
233
+
234
+ def save_sources fn=Redwood::SOURCE_FN, force=false
235
+ @source_mutex.synchronize do
236
+ if @sources_dirty || force
237
+ Redwood::save_yaml_obj sources, fn, false, true
238
+ end
239
+ @sources_dirty = false
240
+ end
241
+ end
242
+ end
243
+
244
+ end