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 Keymap
@@ -5,12 +5,12 @@ class LabelManager
5
5
 
6
6
  ## labels that have special semantics. user will be unable to
7
7
  ## add/remove these via normal label mechanisms.
8
- RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted ]
8
+ RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox ]
9
9
 
10
10
  ## labels which it nonetheless makes sense to search for by
11
- LISTABLE_RESERVED_LABELS = [ :starred, :spam, :draft, :sent, :killed, :deleted ]
11
+ LISTABLE_RESERVED_LABELS = [ :starred, :spam, :draft, :sent, :killed, :deleted, :inbox ]
12
12
 
13
- ## labels that will never be displayed to the user
13
+ ## labels that will typically be hidden from the user
14
14
  HIDDEN_RESERVED_LABELS = [ :starred, :unread ]
15
15
 
16
16
  def initialize fn
@@ -28,17 +28,38 @@ class LabelManager
28
28
  self.class.i_am_the_instance self
29
29
  end
30
30
 
31
- ## all listable (user-defined and system listable) labels, ordered
31
+ ## all listable (just user-defined at the moment) labels, ordered
32
32
  ## nicely and converted to pretty strings. use #label_for to recover
33
33
  ## the original label.
34
- def listable_label_strings
35
- LISTABLE_RESERVED_LABELS.sort_by { |l| l.to_s }.map { |l| l.to_s.ucfirst } +
36
- @labels.keys.map { |l| l.to_s }.sort
34
+ def listable_labels
35
+ ## uniq's only necessary here because of certain upgrade issues
36
+ (LISTABLE_RESERVED_LABELS + @labels.keys).uniq
37
+ end
38
+
39
+ ## all apply-able (user-defined and system listable) labels, ordered
40
+ ## nicely and converted to pretty strings. use #label_for to recover
41
+ ## the original label.
42
+ def applyable_labels
43
+ @labels.keys
37
44
  end
38
45
 
39
46
  ## reverse the label->string mapping, for convenience!
40
- def label_for string
41
- string.downcase.intern
47
+ def string_for l
48
+ if RESERVED_LABELS.include? l
49
+ l.to_s.ucfirst
50
+ else
51
+ l.to_s
52
+ end
53
+ end
54
+
55
+ def label_for s
56
+ l = s.intern
57
+ l2 = s.downcase.intern
58
+ if RESERVED_LABELS.include? l2
59
+ l2
60
+ else
61
+ l
62
+ end
42
63
  end
43
64
 
44
65
  def << t
@@ -25,7 +25,18 @@ class Logger
25
25
  def log s
26
26
  # $stderr.puts s
27
27
  make_buf
28
- @mode << "#{Time.now}: #{s.chomp}\n"
28
+ prefix = "#{Time.now}: "
29
+ padding = " " * prefix.length
30
+ first = true
31
+ s.split(/[\r\n]/).each do |l|
32
+ l = l.chomp
33
+ if first
34
+ first = false
35
+ @mode << "#{prefix}#{l}\n"
36
+ else
37
+ @mode << "#{padding}#{l}\n"
38
+ end
39
+ end
29
40
  $stderr.puts "[#{Time.now}] #{s.chomp}" unless BufferManager.instantiated? && @mode.buffer
30
41
  end
31
42
 
@@ -11,13 +11,15 @@ module Redwood
11
11
  class Maildir < Source
12
12
  SCAN_INTERVAL = 30 # seconds
13
13
 
14
+ ## remind me never to use inheritance again.
14
15
  yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
15
16
  def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[]
16
17
  super uri, last_date, usual, archived, id
17
- uri = URI(uri)
18
+ uri = URI(Source.expand_filesystem_uri(uri))
18
19
 
19
20
  raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
20
21
  raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
22
+ raise ArgumentError, "mbox URI must have a path component" unless uri.path
21
23
 
22
24
  @dir = uri.path
23
25
  @labels = (labels || []).freeze
@@ -27,10 +29,14 @@ class Maildir < Source
27
29
  @mutex = Mutex.new
28
30
  end
29
31
 
32
+ def file_path; @dir end
30
33
  def self.suggest_labels_for path; [] end
34
+ def is_source_for? uri; super || (URI(Source.expand_filesystem_uri(uri)) == URI(self.uri)); end
31
35
 
32
36
  def check
33
37
  scan_mailbox
38
+ return unless start_offset
39
+
34
40
  start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
35
41
  end
36
42
 
@@ -55,7 +61,7 @@ class Maildir < Source
55
61
  ret
56
62
  end
57
63
 
58
- def raw_full_message id
64
+ def raw_message id
59
65
  scan_mailbox
60
66
  with_file_for(id) { |f| f.readlines.join }
61
67
  end
@@ -88,12 +94,14 @@ class Maildir < Source
88
94
 
89
95
  def each
90
96
  scan_mailbox
97
+ return unless start_offset
98
+
91
99
  start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
92
100
 
93
101
  start.upto(@ids.length - 1) do |i|
94
102
  id = @ids[i]
95
103
  self.cur_offset = id
96
- yield id, @labels + (@ids_to_fns[id] =~ /,.*R.*$/ ? [] : [:unread])
104
+ yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])
97
105
  end
98
106
  end
99
107
 
@@ -109,6 +117,20 @@ class Maildir < Source
109
117
 
110
118
  def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
111
119
 
120
+ def draft? msg; maildir_data(msg)[2].include? "D"; end
121
+ def flagged? msg; maildir_data(msg)[2].include? "F"; end
122
+ def passed? msg; maildir_data(msg)[2].include? "P"; end
123
+ def replied? msg; maildir_data(msg)[2].include? "R"; end
124
+ def seen? msg; maildir_data(msg)[2].include? "S"; end
125
+ def trashed? msg; maildir_data(msg)[2].include? "T"; end
126
+
127
+ def mark_draft msg; maildir_mark_file msg, "D" unless draft? msg; end
128
+ def mark_flagged msg; maildir_mark_file msg, "F" unless flagged? msg; end
129
+ def mark_passed msg; maildir_mark_file msg, "P" unless passed? msg; end
130
+ def mark_replied msg; maildir_mark_file msg, "R" unless replied? msg; end
131
+ def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
132
+ def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
133
+
112
134
  private
113
135
 
114
136
  def make_id fn
@@ -124,6 +146,29 @@ private
124
146
  raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
125
147
  end
126
148
  end
149
+
150
+ def maildir_data msg
151
+ fn = File.basename @ids_to_fns[msg]
152
+ fn =~ %r{^([^:,]+):([12]),([DFPRST]*)$}
153
+ [($1 || fn), ($2 || "2"), ($3 || "")]
154
+ end
155
+
156
+ ## not thread-safe on msg
157
+ def maildir_mark_file msg, flag
158
+ orig_path = @ids_to_fns[msg]
159
+ orig_base, orig_fn = File.split(orig_path)
160
+ new_base = orig_base.slice(0..-4) + 'cur'
161
+ tmp_base = orig_base.slice(0..-4) + 'tmp'
162
+ md_base, md_ver, md_flags = maildir_data msg
163
+ md_flags += flag; md_flags = md_flags.split(//).sort.join.squeeze
164
+ new_path = File.join new_base, "#{md_base}:#{md_ver},#{md_flags}"
165
+ tmp_path = File.join tmp_base, "#{md_base}:#{md_ver},#{md_flags}"
166
+ File.link orig_path, tmp_path
167
+ File.unlink orig_path
168
+ File.link tmp_path, new_path
169
+ File.unlink tmp_path
170
+ @ids_to_fns[msg] = new_path
171
+ end
127
172
  end
128
173
 
129
174
  end
@@ -56,7 +56,7 @@ module MBox
56
56
  begin
57
57
  Rfc2047.decode_to $encoding, v
58
58
  rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
59
- Redwood::log "warning: error decoding RFC 2047 header: #{e.message}"
59
+ Redwood::log "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
60
60
  v
61
61
  end
62
62
  end
@@ -6,24 +6,30 @@ module MBox
6
6
 
7
7
  class Loader < Source
8
8
  yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
9
- def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
10
- super uri_or_fp, start_offset, usual, archived, id
11
9
 
10
+ ## uri_or_fp is horrific. need to refactor.
11
+ def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
12
12
  @mutex = Mutex.new
13
- @labels = (labels || []).freeze
13
+ @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
14
14
 
15
15
  case uri_or_fp
16
16
  when String
17
- uri = URI(uri_or_fp)
17
+ uri = URI(Source.expand_filesystem_uri(uri_or_fp))
18
18
  raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
19
- raise ArgumentError, "mbox uri ('#{uri}') cannot have a host: #{uri.host}" if uri.host
19
+ raise ArgumentError, "mbox URI ('#{uri}') cannot have a host: #{uri.host}" if uri.host
20
+ raise ArgumentError, "mbox URI must have a path component" unless uri.path
20
21
  @f = File.open uri.path
22
+ @path = uri.path
21
23
  else
22
24
  @f = uri_or_fp
25
+ @path = uri_or_fp.path
23
26
  end
27
+
28
+ super uri_or_fp, start_offset, usual, archived, id
24
29
  end
25
30
 
26
- def file_path; URI(uri).path end
31
+ def file_path; @path end
32
+ def is_source_for? uri; super || (self.uri.is_a?(String) && (URI(Source.expand_filesystem_uri(uri)) == URI(Source.expand_filesystem_uri(self.uri)))) end
27
33
 
28
34
  def self.suggest_labels_for path
29
35
  ## heuristic: use the filename as a label, unless the file
@@ -62,7 +68,11 @@ class Loader < Source
62
68
  @f.seek offset
63
69
  begin
64
70
  RMail::Mailbox::MBoxReader.new(@f).each_message do |input|
65
- return RMail::Parser.read(input)
71
+ m = RMail::Parser.read(input)
72
+ if m.body && m.body.is_a?(String)
73
+ m.body.gsub!(/^>From /, "From ")
74
+ end
75
+ return m
66
76
  end
67
77
  rescue RMail::Parser::Error => e
68
78
  raise FatalSourceError, "error parsing mbox file: #{e.message}"
@@ -81,19 +91,19 @@ class Loader < Source
81
91
  ret
82
92
  end
83
93
 
84
- def raw_full_message offset
94
+ def raw_message offset
85
95
  ret = ""
86
- each_raw_full_message_line(offset) { |l| ret += l }
96
+ each_raw_message_line(offset) { |l| ret += l }
87
97
  ret
88
98
  end
89
99
 
90
100
  ## apparently it's a million times faster to call this directly if
91
101
  ## we're just moving messages around on disk, than reading things
92
- ## into memory with raw_full_message.
102
+ ## into memory with raw_message.
93
103
  ##
94
104
  ## i hoped never to have to move shit around on disk but
95
105
  ## sup-sync-back has to do it.
96
- def each_raw_full_message_line offset
106
+ def each_raw_message_line offset
97
107
  @mutex.synchronize do
98
108
  @f.seek offset
99
109
  yield @f.gets
@@ -137,7 +147,7 @@ class Loader < Source
137
147
  end
138
148
 
139
149
  self.cur_offset = next_offset
140
- [returned_offset, @labels]
150
+ [returned_offset, (@labels + [:unread]).uniq]
141
151
  end
142
152
  end
143
153
 
@@ -65,7 +65,7 @@ class SSHLoader < Source
65
65
  end
66
66
  end
67
67
 
68
- [:start_offset, :load_header, :load_message, :raw_header, :raw_full_message].each do |meth|
68
+ [:start_offset, :load_header, :load_message, :raw_header, :raw_message].each do |meth|
69
69
  define_method(meth) { |*a| safely { @loader.send meth, *a } }
70
70
  end
71
71
  end
@@ -0,0 +1,198 @@
1
+ ## Here we define all the "chunks" that a message is parsed
2
+ ## into. Chunks are used by ThreadViewMode to render a message. Chunks
3
+ ## are used for both MIME stuff like attachments, for Sup's parsing of
4
+ ## the message body into text, quote, and signature regions, and for
5
+ ## notices like "this message was decrypted" or "this message contains
6
+ ## a valid signature"---basically, anything we want to differentiate
7
+ ## at display time.
8
+ ##
9
+ ## A chunk can be inlineable, expandable, or viewable. If it's
10
+ ## inlineable, #color and #lines are called and the output is treated
11
+ ## as part of the message text. This is how Text and one-line Quotes
12
+ ## and Signatures work.
13
+ ##
14
+ ## If it's not inlineable but is expandable, #patina_color and
15
+ ## #patina_text are called to generate a "patina" (a one-line widget,
16
+ ## basically), and the user can press enter to toggle the display of
17
+ ## the chunk content, which is generated from #color and #lines as
18
+ ## above. This is how Quote, Signature, and most widgets
19
+ ## work. Exandable chunks can additionally define #initial_state to be
20
+ ## :open if they want to start expanded (default is to start collapsed).
21
+ ##
22
+ ## If it's not expandable but is viewable, a patina is displayed using
23
+ ###patina_color and #patina_text, but no toggling is allowed. Instead,
24
+ ##if #view! is defined, pressing enter on the widget calls view! and
25
+ ##(if that returns false) #to_s. Otherwise, enter does nothing. This
26
+ ##is how non-inlineable attachments work.
27
+
28
+ module Redwood
29
+ module Chunk
30
+ class Attachment
31
+ HookManager.register "mime-decode", <<EOS
32
+ Executes when decoding a MIME attachment.
33
+ Variables:
34
+ content_type: the content-type of the message
35
+ filename: the filename of the attachment as saved to disk (generated
36
+ on the fly, so don't call more than once)
37
+ sibling_types: if this attachment is part of a multipart MIME attachment,
38
+ an array of content-types for all attachments. Otherwise,
39
+ the empty array.
40
+ Return value:
41
+ The decoded text of the attachment, or nil if not decoded.
42
+ EOS
43
+ #' stupid ruby-mode
44
+
45
+ ## raw_content is the post-MIME-decode content. this is used for
46
+ ## saving the attachment to disk.
47
+ attr_reader :content_type, :filename, :lines, :raw_content
48
+
49
+ def initialize content_type, filename, encoded_content, sibling_types
50
+ @content_type = content_type
51
+ @filename = filename
52
+ @raw_content =
53
+ if encoded_content.body
54
+ encoded_content.decode
55
+ else
56
+ "For some bizarre reason, RubyMail was unable to parse this attachment.\n"
57
+ end
58
+
59
+ @lines =
60
+ case @content_type
61
+ when /^text\/plain\b/
62
+ Message.convert_from(@raw_content, encoded_content.charset).split("\n")
63
+ else
64
+ text = HookManager.run "mime-decode", :content_type => content_type,
65
+ :filename => lambda { write_to_disk },
66
+ :sibling_types => sibling_types
67
+ text.split("\n") if text
68
+ end
69
+ end
70
+
71
+ def color; :none end
72
+ def patina_color; :attachment_color end
73
+ def patina_text
74
+ if expandable?
75
+ "Attachment: #{filename} (#{lines.length} lines)"
76
+ else
77
+ "Attachment: #{filename} (#{content_type})"
78
+ end
79
+ end
80
+
81
+ ## an attachment is exapndable if we've managed to decode it into
82
+ ## something we can display inline. otherwise, it's viewable.
83
+ def inlineable?; false end
84
+ def expandable?; !viewable? end
85
+ def initial_state; :open end
86
+ def viewable?; @lines.nil? end
87
+ def view!
88
+ path = write_to_disk
89
+ system "/usr/bin/run-mailcap --action=view #{@content_type}:#{path} >& /dev/null"
90
+ $? == 0
91
+ end
92
+
93
+ def write_to_disk
94
+ file = Tempfile.new "redwood.attachment"
95
+ file.print @raw_content
96
+ file.close
97
+ file.path
98
+ end
99
+
100
+ ## used when viewing the attachment as text
101
+ def to_s
102
+ @lines || @raw_content
103
+ end
104
+ end
105
+
106
+ class Text
107
+ WRAP_LEN = 80 # wrap at this width
108
+
109
+ attr_reader :lines
110
+ def initialize lines
111
+ @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten # wrap
112
+
113
+ ## trim off all empty lines except one
114
+ lines.pop while lines.last =~ /^\s*$/
115
+ end
116
+
117
+ def inlineable?; true end
118
+ def expandable?; false end
119
+ def viewable?; false end
120
+ def color; :none end
121
+ end
122
+
123
+ class Quote
124
+ attr_reader :lines
125
+ def initialize lines
126
+ @lines = lines
127
+ end
128
+
129
+ def inlineable?; @lines.length == 1 end
130
+ def expandable?; !inlineable? end
131
+ def viewable?; false end
132
+
133
+ def patina_color; :quote_patina_color end
134
+ def patina_text; "(#{lines.length} quoted lines)" end
135
+ def color; :quote_color end
136
+ end
137
+
138
+ class Signature
139
+ attr_reader :lines
140
+ def initialize lines
141
+ @lines = lines
142
+ end
143
+
144
+ def inlineable?; @lines.length == 1 end
145
+ def expandable?; !inlineable? end
146
+ def viewable?; false end
147
+
148
+ def patina_color; :sig_patina_color end
149
+ def patina_text; "(#{lines.length}-line signature)" end
150
+ def color; :sig_color end
151
+ end
152
+
153
+ class EnclosedMessage
154
+ attr_reader :lines
155
+ def initialize from, body
156
+ @from = from
157
+ @lines = body.split "\n"
158
+ end
159
+
160
+ def from
161
+ @from ? @from.longname : "unknown sender"
162
+ end
163
+
164
+ def inlineable?; false end
165
+ def expandable?; true end
166
+ def initial_state; :open end
167
+ def viewable?; false end
168
+
169
+ def patina_color; :generic_notice_patina_color end
170
+ def patina_text; "Begin enclosed message from #{from} (#{@lines.length} lines)" end
171
+
172
+ def color; :quote_color end
173
+ end
174
+
175
+ class CryptoNotice
176
+ attr_reader :lines, :status, :patina_text
177
+
178
+ def initialize status, description, lines=[]
179
+ @status = status
180
+ @patina_text = description
181
+ @lines = lines
182
+ end
183
+
184
+ def patina_color
185
+ case status
186
+ when :valid: :cryptosig_valid_color
187
+ when :invalid: :cryptosig_invalid_color
188
+ else :cryptosig_unknown_color
189
+ end
190
+ end
191
+ def color; patina_color end
192
+
193
+ def inlineable?; false end
194
+ def expandable?; !@lines.empty? end
195
+ def viewable?; false end
196
+ end
197
+ end
198
+ end