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