sup 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (48) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +5 -1
  3. data/Rakefile +2 -1
  4. data/bin/sup +27 -10
  5. data/bin/sup-add +2 -1
  6. data/bin/sup-sync-back +51 -23
  7. data/doc/FAQ.txt +29 -37
  8. data/doc/Hooks.txt +38 -0
  9. data/doc/{UserGuide.txt → NewUserGuide.txt} +27 -21
  10. data/doc/TODO +91 -57
  11. data/lib/sup.rb +17 -1
  12. data/lib/sup/buffer.rb +80 -16
  13. data/lib/sup/colormap.rb +0 -2
  14. data/lib/sup/contact.rb +3 -2
  15. data/lib/sup/crypto.rb +110 -0
  16. data/lib/sup/draft.rb +2 -6
  17. data/lib/sup/hook.rb +131 -0
  18. data/lib/sup/imap.rb +27 -16
  19. data/lib/sup/index.rb +38 -14
  20. data/lib/sup/keymap.rb +0 -2
  21. data/lib/sup/label.rb +30 -9
  22. data/lib/sup/logger.rb +12 -1
  23. data/lib/sup/maildir.rb +48 -3
  24. data/lib/sup/mbox.rb +1 -1
  25. data/lib/sup/mbox/loader.rb +22 -12
  26. data/lib/sup/mbox/ssh-loader.rb +1 -1
  27. data/lib/sup/message-chunks.rb +198 -0
  28. data/lib/sup/message.rb +154 -115
  29. data/lib/sup/modes/compose-mode.rb +18 -0
  30. data/lib/sup/modes/contact-list-mode.rb +1 -1
  31. data/lib/sup/modes/edit-message-mode.rb +112 -31
  32. data/lib/sup/modes/file-browser-mode.rb +1 -1
  33. data/lib/sup/modes/inbox-mode.rb +1 -1
  34. data/lib/sup/modes/label-list-mode.rb +8 -6
  35. data/lib/sup/modes/label-search-results-mode.rb +4 -1
  36. data/lib/sup/modes/log-mode.rb +1 -1
  37. data/lib/sup/modes/reply-mode.rb +18 -16
  38. data/lib/sup/modes/search-results-mode.rb +1 -1
  39. data/lib/sup/modes/thread-index-mode.rb +61 -33
  40. data/lib/sup/modes/thread-view-mode.rb +111 -102
  41. data/lib/sup/person.rb +5 -1
  42. data/lib/sup/poll.rb +36 -7
  43. data/lib/sup/sent.rb +1 -0
  44. data/lib/sup/source.rb +7 -3
  45. data/lib/sup/textfield.rb +48 -34
  46. data/lib/sup/thread.rb +9 -5
  47. data/lib/sup/util.rb +16 -22
  48. metadata +7 -3
@@ -1,5 +1,3 @@
1
- require "curses"
2
-
3
1
  module Redwood
4
2
 
5
3
  class Colormap
@@ -10,7 +10,7 @@ class ContactManager
10
10
 
11
11
  if File.exists? fn
12
12
  IO.foreach(fn) do |l|
13
- l =~ /^(\S+): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
13
+ l =~ /^([^:]+): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
14
14
  aalias, addr = $1, $2
15
15
  p = PersonManager.person_for addr, :definitive => true
16
16
  @p2a[p] = aalias
@@ -35,9 +35,10 @@ class ContactManager
35
35
  @a2p.delete aalias
36
36
  end
37
37
  end
38
- def person_with aalias; @a2p[aalias]; end
38
+ def contact_for aalias; @a2p[aalias]; end
39
39
  def alias_for person; @p2a[person]; end
40
40
  def is_contact? person; @p2a.member? person; end
41
+
41
42
  def save
42
43
  File.open(@fn, "w") do |f|
43
44
  @p2a.each do |p, a|
@@ -0,0 +1,110 @@
1
+ module Redwood
2
+
3
+ class CryptoManager
4
+ include Singleton
5
+
6
+ def initialize
7
+ @mutex = Mutex.new
8
+ self.class.i_am_the_instance self
9
+
10
+ bin = `which gpg`.chomp
11
+ bin = `which pgp`.chomp unless bin =~ /\S/
12
+
13
+ @cmd =
14
+ case bin
15
+ when /\S/
16
+ "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
17
+ else
18
+ nil
19
+ end
20
+ end
21
+
22
+ # returns a cryptosignature
23
+ def verify payload, signature # both RubyMail::Message objects
24
+ return unknown_status(cant_find_binary) unless @cmd
25
+
26
+ payload_fn = Tempfile.new "redwood.payload"
27
+ payload_fn.write payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
28
+ payload_fn.close
29
+
30
+ signature_fn = Tempfile.new "redwood.signature"
31
+ signature_fn.write signature.decode
32
+ signature_fn.close
33
+
34
+ cmd = "#{@cmd} --verify #{signature_fn.path} #{payload_fn.path} 2> /dev/null"
35
+
36
+ #Redwood::log "gpg: running: #{cmd}"
37
+ gpg_output = `#{cmd}`
38
+ #Redwood::log "got output: #{gpg_output.inspect}"
39
+ output_lines = gpg_output.split(/\n/)
40
+
41
+ if gpg_output =~ /^gpg: (.* signature from .*$)/
42
+ if $? == 0
43
+ Chunk::CryptoNotice.new :valid, $1, output_lines
44
+ else
45
+ Chunk::CryptoNotice.new :invalid, $1, output_lines
46
+ end
47
+ else
48
+ unknown_status output_lines
49
+ end
50
+ end
51
+
52
+ # returns decrypted_message, status, desc, lines
53
+ def decrypt payload # RubyMail::Message objects
54
+ return unknown_status(cant_find_binary) unless @cmd
55
+
56
+ # cmd = "#{@cmd} --decrypt 2> /dev/null"
57
+
58
+ # Redwood::log "gpg: running: #{cmd}"
59
+
60
+ # gpg_output =
61
+ # IO.popen(cmd, "a+") do |f|
62
+ # f.puts payload.to_s
63
+ # f.gets
64
+ # end
65
+
66
+ payload_fn = Tempfile.new "redwood.payload"
67
+ payload_fn.write payload.to_s
68
+ payload_fn.close
69
+
70
+ cmd = "#{@cmd} --decrypt #{payload_fn.path} 2> /dev/null"
71
+ Redwood::log "gpg: running: #{cmd}"
72
+ gpg_output = `#{cmd}`
73
+ Redwood::log "got output: #{gpg_output.inspect}"
74
+
75
+ if $? == 0 # successful decryption
76
+ decrypted_payload, sig_lines =
77
+ if gpg_output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
78
+ [$1, $2]
79
+ else
80
+ [gpg_output, nil]
81
+ end
82
+
83
+ sig =
84
+ if sig_lines # encrypted & signed
85
+ if sig_lines =~ /^gpg: (Good signature from .*$)/
86
+ Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
87
+ else
88
+ Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
89
+ end
90
+ end
91
+
92
+ notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
93
+ [RMail::Parser.read(decrypted_payload), sig, notice]
94
+ else
95
+ notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
96
+ [nil, nil, notice]
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def unknown_status lines=[]
103
+ Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
104
+ end
105
+
106
+ def cant_find_binary
107
+ ["Can't find gpg or pgp binary in path"]
108
+ end
109
+ end
110
+ end
@@ -99,12 +99,8 @@ class DraftLoader < Source
99
99
  ret
100
100
  end
101
101
 
102
- def raw_full_message offset
103
- ret = ""
104
- File.open fn_for_offset(offset) do |f|
105
- ret += l until f.eof?
106
- end
107
- ret
102
+ def raw_message offset
103
+ IO.readlines(fn_for_offset(offset)).join
108
104
  end
109
105
 
110
106
  def start_offset; 0; end
@@ -0,0 +1,131 @@
1
+ module Redwood
2
+
3
+ class HookManager
4
+ ## there's probably a better way to do this, but to evaluate a hook
5
+ ## with a bunch of pre-set "local variables" i define a function
6
+ ## per variable and then instance_evaluate the code.
7
+ ##
8
+ ## how does rails do it, when you pass :locals into a partial?
9
+ ##
10
+ ## i don't bother providing setters, since i'm pretty sure the
11
+ ## charade will fall apart pretty quickly with respect to scoping.
12
+ ## this is basically fail-fast.
13
+ class HookContext
14
+ def initialize name
15
+ @__name = name
16
+ @__locals = {}
17
+ end
18
+
19
+ attr_writer :__locals
20
+
21
+ def method_missing m, *a
22
+ case @__locals[m]
23
+ when Proc
24
+ @__locals[m].call(*a)
25
+ when nil
26
+ super
27
+ else
28
+ @__locals[m]
29
+ end
30
+ end
31
+
32
+ def say s
33
+ @__say_id = BufferManager.say s, @__say_id
34
+ BufferManager.draw_screen
35
+ end
36
+
37
+ def log s
38
+ Redwood::log "hook[#@__name]: #{s}"
39
+ end
40
+
41
+ def ask_yes_or_no q
42
+ BufferManager.ask_yes_or_no q
43
+ end
44
+
45
+ def __binding
46
+ binding
47
+ end
48
+
49
+ def __cleanup
50
+ BufferManager.clear @__say_id if @__say_id
51
+ end
52
+ end
53
+
54
+ include Singleton
55
+
56
+ def initialize dir
57
+ @dir = dir
58
+ @hooks = {}
59
+ @descs = {}
60
+ @contexts = {}
61
+
62
+ Dir.mkdir dir unless File.exists? dir
63
+
64
+ self.class.i_am_the_instance self
65
+ end
66
+
67
+ def run name, locals={}
68
+ hook = hook_for(name) or return
69
+ context = @contexts[hook] ||= HookContext.new(name)
70
+ context.__locals = locals
71
+
72
+ result = nil
73
+ begin
74
+ result = context.instance_eval @hooks[name], fn_for(name)
75
+ rescue Exception => e
76
+ log "error running hook: #{e.message}"
77
+ log e.backtrace.join("\n")
78
+ BufferManager.flash "Error running hook: #{e.message}"
79
+ end
80
+ context.__cleanup
81
+ result
82
+ end
83
+
84
+ def register name, desc
85
+ @descs[name] = desc
86
+ end
87
+
88
+ def print_hooks f=$stdout
89
+ puts <<EOS
90
+ Have #{@descs.size} registered hooks:
91
+
92
+ EOS
93
+
94
+ @descs.sort.each do |name, desc|
95
+ f.puts <<EOS
96
+ #{name}
97
+ #{"-" * name.length}
98
+ File: #{fn_for name}
99
+ #{desc}
100
+ EOS
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def hook_for name
107
+ unless @hooks.member? name
108
+ @hooks[name] =
109
+ begin
110
+ returning IO.readlines(fn_for(name)).join do
111
+ log "read '#{name}' from #{fn_for(name)}"
112
+ end
113
+ rescue SystemCallError => e
114
+ #log "disabled hook for '#{name}': #{e.message}"
115
+ nil
116
+ end
117
+ end
118
+
119
+ @hooks[name]
120
+ end
121
+
122
+ def fn_for name
123
+ File.join @dir, "#{name}.rb"
124
+ end
125
+
126
+ def log m
127
+ Redwood::log("hook: " + m)
128
+ end
129
+ end
130
+
131
+ end
@@ -3,6 +3,7 @@ require 'net/imap'
3
3
  require 'stringio'
4
4
  require 'time'
5
5
  require 'rmail'
6
+ require 'cgi'
6
7
 
7
8
  ## fucking imap fucking sucks. what the FUCK kind of committee of
8
9
  ## dunces designed this shit.
@@ -62,10 +63,10 @@ class IMAP < Source
62
63
  @username = username
63
64
  @password = password
64
65
  @imap = nil
65
- @imap_ids = {}
66
+ @imap_state = {}
66
67
  @ids = []
67
68
  @last_scan = nil
68
- @labels = (labels || []).freeze
69
+ @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
69
70
  @say_id = nil
70
71
  @mutex = Mutex.new
71
72
  end
@@ -82,11 +83,13 @@ class IMAP < Source
82
83
  def port; @parsed_uri.port || (ssl? ? 993 : 143); end
83
84
  def mailbox
84
85
  x = @parsed_uri.path[1..-1]
85
- x.nil? || x.empty? ? 'INBOX' : x
86
+ (x.nil? || x.empty?) ? 'INBOX' : CGI.unescape(x)
86
87
  end
87
88
  def ssl?; @parsed_uri.scheme == 'imaps' end
88
89
 
89
90
  def check
91
+ return unless start_offset
92
+
90
93
  ids =
91
94
  @mutex.synchronize do
92
95
  unsynchronized_scan_mailbox
@@ -104,23 +107,21 @@ class IMAP < Source
104
107
  end
105
108
 
106
109
  def load_message id
107
- RMail::Parser.read raw_full_message(id)
110
+ RMail::Parser.read raw_message(id)
108
111
  end
109
112
 
110
113
  def raw_header id
111
114
  unsynchronized_scan_mailbox
112
- header, flags = get_imap_fields id, 'RFC822.HEADER', 'FLAGS'
113
- ## very bad. this is very very bad. very bad bad bad.
114
- header = header + "Status: RO\n" if flags.include? :Seen # fake an mbox-style read header # TODO: improve source-marked-as-read reporting system
115
+ header, flags = get_imap_fields id, 'RFC822.HEADER'
115
116
  header.gsub(/\r\n/, "\n")
116
117
  end
117
118
  synchronized :raw_header
118
119
 
119
- def raw_full_message id
120
+ def raw_message id
120
121
  unsynchronized_scan_mailbox
121
122
  get_imap_fields(id, 'RFC822').first.gsub(/\r\n/, "\n")
122
123
  end
123
- synchronized :raw_full_message
124
+ synchronized :raw_message
124
125
 
125
126
  def connect
126
127
  return if @imap
@@ -140,15 +141,17 @@ class IMAP < Source
140
141
 
141
142
  range = (@ids.length + 1) .. last_id
142
143
  Redwood::log "fetching IMAP headers #{range}"
143
- fetch(range, ['RFC822.SIZE', 'INTERNALDATE']).each do |v|
144
+ fetch(range, ['RFC822.SIZE', 'INTERNALDATE', 'FLAGS']).each do |v|
144
145
  id = make_id v
145
146
  @ids << id
146
- @imap_ids[id] = v.seqno
147
+ @imap_state[id] = { :id => v.seqno, :flags => v.attr["FLAGS"] }
147
148
  end
148
149
  end
149
150
  synchronized :scan_mailbox
150
151
 
151
152
  def each
153
+ return unless start_offset
154
+
152
155
  ids =
153
156
  @mutex.synchronize do
154
157
  unsynchronized_scan_mailbox
@@ -157,10 +160,18 @@ class IMAP < Source
157
160
 
158
161
  start = ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}."
159
162
 
160
- start.upto(ids.length - 1) do |i|
163
+ start.upto(ids.length - 1) do |i|
161
164
  id = ids[i]
162
- self.cur_offset = id
163
- yield id, @labels
165
+ state = @mutex.synchronize { @imap_state[id] } or next
166
+ self.cur_offset = id
167
+ labels = { :Seen => :unread,
168
+ :Flagged => :starred,
169
+ :Deleted => :deleted
170
+ }.inject(@labels) do |cur, (imap, sup)|
171
+ cur + (state[:flags].include?(imap) ? [sup] : [])
172
+ end
173
+
174
+ yield id, labels
164
175
  end
165
176
  end
166
177
 
@@ -259,9 +270,9 @@ private
259
270
  end
260
271
 
261
272
  def get_imap_fields id, *fields
262
- imap_id = @imap_ids[id] or raise OutOfSyncSourceError, "Unknown message id #{id}"
273
+ raise OutOfSyncSourceError, "Unknown message id #{id}" unless @imap_state[id]
263
274
 
264
- retried = false
275
+ imap_id = @imap_state[id][:id]
265
276
  result = fetch(imap_id, (fields + ['RFC822.SIZE', 'INTERNALDATE']).uniq).first
266
277
  got_id = make_id result
267
278
  raise OutOfSyncSourceError, "IMAP message mismatch: requested #{id}, got #{got_id}." unless got_id == id
@@ -154,11 +154,10 @@ EOS
154
154
  docid, entry = load_entry_for_id m.id unless docid && entry
155
155
 
156
156
  raise "no source info for message #{m.id}" unless m.source && m.source_info
157
- raise "trying deleting non-corresponding entry #{docid}" if docid && @index[docid][:message_id] != m.id
157
+ raise "trying to delete non-corresponding entry #{docid} with index message-id #{@index[docid][:message_id].inspect} and parameter message id #{m.id.inspect}" if docid && @index[docid][:message_id] != m.id
158
158
 
159
159
  source_id =
160
160
  if m.source.is_a? Integer
161
- raise "Debugging: integer source set"
162
161
  m.source
163
162
  else
164
163
  m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
@@ -184,7 +183,7 @@ EOS
184
183
 
185
184
  docid, entry = load_entry_for_id m.id
186
185
  ## this hasn't been triggered in a long time. TODO: decide whether it's still a problem.
187
- raise "just added message #{m.id} but couldn't find it in a search" unless docid
186
+ raise "just added message #{m.id.inspect} but couldn't find it in a search" unless docid
188
187
  true
189
188
  end
190
189
 
@@ -226,10 +225,12 @@ EOS
226
225
  ## message-building lambdas, so that building an unwanted message
227
226
  ## can be skipped in the block if desired.
228
227
  ##
229
- ## stops loading any thread if a message with a :killed flag is found.
228
+ ## only two options, :limit and :skip_killed. if :skip_killed is
229
+ ## true, stops loading any thread if a message with a :killed flag
230
+ ## is found.
230
231
  SAME_SUBJECT_DATE_LIMIT = 7
231
232
  def each_message_in_thread_for m, opts={}
232
- Redwood::log "Building thread for #{m.id}: #{m.subj}"
233
+ #Redwood::log "Building thread for #{m.id}: #{m.subj}"
233
234
  messages = {}
234
235
  searched = {}
235
236
  num_queries = 0
@@ -262,23 +263,33 @@ EOS
262
263
  q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
263
264
  q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
264
265
 
265
- q = build_query :qobj => q, :load_killed => true
266
+ q = build_query :qobj => q
266
267
 
267
268
  num_queries += 1
269
+ killed = false
268
270
  @index.search_each(q, :limit => :all) do |docid, score|
269
271
  break if opts[:limit] && messages.size >= opts[:limit]
270
- break if @index[docid][:label].split(/\s+/).include? "killed" unless opts[:load_killed]
272
+ if @index[docid][:label].split(/\s+/).include?("killed") && opts[:skip_killed]
273
+ killed = true
274
+ break
275
+ end
271
276
  mid = @index[docid][:message_id]
272
277
  unless messages.member?(mid)
273
- Redwood::log "got #{mid} as a child of #{id}"
278
+ #Redwood::log "got #{mid} as a child of #{id}"
274
279
  messages[mid] ||= lambda { build_message docid }
275
280
  refs = @index[docid][:refs].split(" ")
276
281
  pending += refs
277
282
  end
278
283
  end
279
284
  end
280
- Redwood::log "ran #{num_queries} queries to build thread of #{messages.size + 1} messages for #{m.id}" if num_queries > 0
281
- messages.each { |mid, builder| yield mid, builder }
285
+ if killed
286
+ Redwood::log "thread for #{m.id} is killed, ignoring"
287
+ false
288
+ else
289
+ Redwood::log "ran #{num_queries} queries to build thread of #{messages.size + 1} messages for #{m.id}: #{m.subj}" if num_queries > 0
290
+ messages.each { |mid, builder| yield mid, builder }
291
+ true
292
+ end
282
293
  end
283
294
 
284
295
  ## builds a message object from a ferret result
@@ -294,7 +305,7 @@ EOS
294
305
  "from" => doc[:from],
295
306
  "to" => doc[:to],
296
307
  "message-id" => doc[:message_id],
297
- "references" => doc[:refs],
308
+ "references" => doc[:refs].split(/\s+/).map { |x| "<#{x}>" }.join(" "),
298
309
  }
299
310
 
300
311
  Message.new :source => source, :source_info => doc[:source_info].to_i,
@@ -330,7 +341,7 @@ EOS
330
341
  num = h[:num] || 20
331
342
  @index.search_each(q, :sort => "date DESC", :limit => :all) do |docid, score|
332
343
  break if contacts.size >= num
333
- #Redwood::log "got message with to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
344
+ #Redwood::log "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
334
345
  f = @index[docid][:from]
335
346
  t = @index[docid][:to]
336
347
 
@@ -359,7 +370,20 @@ EOS
359
370
 
360
371
  protected
361
372
 
362
- def parse_user_query_string str; @qparser.parse str; end
373
+ def parse_user_query_string str
374
+ str2 = str.gsub(/(to|from):(\S+)/) do
375
+ field, name = $1, $2
376
+ if(p = ContactManager.contact_for(name))
377
+ [field, p.email]
378
+ else
379
+ [field, name]
380
+ end.join(":")
381
+ end
382
+
383
+ Redwood::log "translated #{str} to #{str2}" unless str2 == str
384
+ @qparser.parse str2
385
+ end
386
+
363
387
  def build_query opts
364
388
  query = Ferret::Search::BooleanQuery.new
365
389
  query.add_query opts[:qobj], :must if opts[:qobj]
@@ -376,7 +400,7 @@ protected
376
400
 
377
401
  query.add_query Ferret::Search::TermQuery.new("label", "spam"), :must_not unless opts[:load_spam] || labels.include?(:spam)
378
402
  query.add_query Ferret::Search::TermQuery.new("label", "deleted"), :must_not unless opts[:load_deleted] || labels.include?(:deleted)
379
- query.add_query Ferret::Search::TermQuery.new("label", "killed"), :must_not unless opts[:load_killed] || labels.include?(:killed)
403
+ query.add_query Ferret::Search::TermQuery.new("label", "killed"), :must_not if opts[:skip_killed]
380
404
  query
381
405
  end
382
406