sup 0.7 → 0.8

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 (46) hide show
  1. data/CONTRIBUTORS +8 -3
  2. data/History.txt +19 -0
  3. data/README.txt +45 -44
  4. data/ReleaseNotes +6 -0
  5. data/bin/sup +36 -5
  6. data/bin/sup-add +0 -0
  7. data/bin/sup-config +0 -0
  8. data/bin/sup-dump +0 -0
  9. data/bin/sup-recover-sources +8 -12
  10. data/bin/sup-sync +22 -16
  11. data/bin/sup-sync-back +1 -1
  12. data/bin/sup-tweak-labels +8 -8
  13. data/lib/sup.rb +3 -17
  14. data/lib/sup/account.rb +2 -3
  15. data/lib/sup/buffer.rb +21 -10
  16. data/lib/sup/colormap.rb +30 -27
  17. data/lib/sup/contact.rb +1 -1
  18. data/lib/sup/draft.rb +1 -3
  19. data/lib/sup/imap.rb +1 -1
  20. data/lib/sup/index.rb +70 -48
  21. data/lib/sup/label.rb +12 -10
  22. data/lib/sup/logger.rb +1 -1
  23. data/lib/sup/maildir.rb +1 -1
  24. data/lib/sup/mbox.rb +13 -70
  25. data/lib/sup/mbox/loader.rb +26 -15
  26. data/lib/sup/message-chunks.rb +18 -6
  27. data/lib/sup/message.rb +56 -67
  28. data/lib/sup/mode.rb +2 -1
  29. data/lib/sup/modes/buffer-list-mode.rb +6 -2
  30. data/lib/sup/modes/compose-mode.rb +0 -1
  31. data/lib/sup/modes/contact-list-mode.rb +1 -1
  32. data/lib/sup/modes/edit-message-mode.rb +37 -9
  33. data/lib/sup/modes/inbox-mode.rb +34 -0
  34. data/lib/sup/modes/label-list-mode.rb +10 -3
  35. data/lib/sup/modes/reply-mode.rb +24 -13
  36. data/lib/sup/modes/resume-mode.rb +2 -0
  37. data/lib/sup/modes/scroll-mode.rb +10 -9
  38. data/lib/sup/modes/search-results-mode.rb +2 -2
  39. data/lib/sup/modes/thread-index-mode.rb +157 -38
  40. data/lib/sup/modes/thread-view-mode.rb +27 -11
  41. data/lib/sup/person.rb +22 -73
  42. data/lib/sup/poll.rb +18 -20
  43. data/lib/sup/source.rb +44 -0
  44. data/lib/sup/undo.rb +39 -0
  45. data/lib/sup/util.rb +25 -16
  46. metadata +46 -45
@@ -7,9 +7,6 @@ class LabelManager
7
7
  ## add/remove these via normal label mechanisms.
8
8
  RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox, :attachment ]
9
9
 
10
- ## labels which it nonetheless makes sense to search for by
11
- LISTABLE_RESERVED_LABELS = [ :starred, :spam, :draft, :sent, :killed, :deleted, :inbox, :attachment ]
12
-
13
10
  ## labels that will typically be hidden from the user
14
11
  HIDDEN_RESERVED_LABELS = [ :starred, :unread, :attachment ]
15
12
 
@@ -22,31 +19,34 @@ class LabelManager
22
19
  []
23
20
  end
24
21
  @labels = {}
22
+ @new_labels = {}
25
23
  @modified = false
26
24
  labels.each { |t| @labels[t] = true }
27
25
 
28
26
  self.class.i_am_the_instance self
29
27
  end
30
28
 
31
- ## all listable (just user-defined at the moment) labels, ordered
29
+ def new_label? l; @new_labels.include?(l) end
30
+
31
+ ## all labels user-defined and system, ordered
32
32
  ## nicely and converted to pretty strings. use #label_for to recover
33
33
  ## the original label.
34
- def listable_labels
34
+ def all_labels
35
35
  ## uniq's only necessary here because of certain upgrade issues
36
- (LISTABLE_RESERVED_LABELS + @labels.keys).uniq
36
+ (RESERVED_LABELS + @labels.keys).uniq
37
37
  end
38
38
 
39
- ## all apply-able (user-defined and system listable) labels, ordered
39
+ ## all user-defined labels, ordered
40
40
  ## nicely and converted to pretty strings. use #label_for to recover
41
41
  ## the original label.
42
- def applyable_labels
42
+ def user_defined_labels
43
43
  @labels.keys
44
44
  end
45
45
 
46
46
  ## reverse the label->string mapping, for convenience!
47
47
  def string_for l
48
48
  if RESERVED_LABELS.include? l
49
- l.to_s.ucfirst
49
+ l.to_s.capitalize
50
50
  else
51
51
  l.to_s
52
52
  end
@@ -66,12 +66,13 @@ class LabelManager
66
66
  t = t.intern unless t.is_a? Symbol
67
67
  unless @labels.member?(t) || RESERVED_LABELS.member?(t)
68
68
  @labels[t] = true
69
+ @new_labels[t] = true
69
70
  @modified = true
70
71
  end
71
72
  end
72
73
 
73
74
  def delete t
74
- if @labels.delete t
75
+ if @labels.delete(t)
75
76
  @modified = true
76
77
  end
77
78
  end
@@ -79,6 +80,7 @@ class LabelManager
79
80
  def save
80
81
  return unless @modified
81
82
  File.open(@fn, "w") { |f| f.puts @labels.keys.sort_by { |l| l.to_s } }
83
+ @new_labels = {}
82
84
  end
83
85
  end
84
86
 
@@ -18,7 +18,7 @@ class Logger
18
18
  def make_buf
19
19
  return if @mode.buffer || !BufferManager.instantiated? || !@respawn || @spawning
20
20
  @spawning = true
21
- @mode.buffer = BufferManager.instance.spawn "<log>", @mode, :hidden => true
21
+ @mode.buffer = BufferManager.instance.spawn "log", @mode, :hidden => true, :system => true
22
22
  @spawning = false
23
23
  end
24
24
 
@@ -56,7 +56,7 @@ class Maildir < Source
56
56
 
57
57
  def load_header id
58
58
  scan_mailbox
59
- with_file_for(id) { |f| MBox::read_header f }
59
+ with_file_for(id) { |f| parse_raw_email_header f }
60
60
  end
61
61
 
62
62
  def load_message id
@@ -1,81 +1,24 @@
1
1
  require "sup/mbox/loader"
2
2
  require "sup/mbox/ssh-file"
3
3
  require "sup/mbox/ssh-loader"
4
- require "sup/rfc2047"
5
4
 
6
5
  module Redwood
7
6
 
8
- ## some utility functions. actually these are not mbox-specific at all
9
- ## and should be moved somewhere else.
10
- ##
11
- ## TODO: move functionality to somewhere better, like message.rb
12
7
  module MBox
13
- BREAK_RE = /^From \S+/
14
- HEADER_RE = /\s*(.*?)\s*/
15
-
16
- def read_header f
17
- header = {}
18
- last = nil
19
-
20
- ## i do it in this weird way because i am trying to speed things up
21
- ## when scanning over large mbox files.
22
- while(line = f.gets)
23
- case line
24
- ## these three can occur multiple times, and we want the first one
25
- when /^(Delivered-To):#{HEADER_RE}$/i,
26
- /^(X-Original-To):#{HEADER_RE}$/i,
27
- /^(Envelope-To):#{HEADER_RE}$/i: header[last = $1] ||= $2
28
-
29
- when /^(From):#{HEADER_RE}$/i,
30
- /^(To):#{HEADER_RE}$/i,
31
- /^(Cc):#{HEADER_RE}$/i,
32
- /^(Bcc):#{HEADER_RE}$/i,
33
- /^(Subject):#{HEADER_RE}$/i,
34
- /^(Date):#{HEADER_RE}$/i,
35
- /^(References):#{HEADER_RE}$/i,
36
- /^(In-Reply-To):#{HEADER_RE}$/i,
37
- /^(Reply-To):#{HEADER_RE}$/i,
38
- /^(List-Post):#{HEADER_RE}$/i,
39
- /^(List-Subscribe):#{HEADER_RE}$/i,
40
- /^(List-Unsubscribe):#{HEADER_RE}$/i,
41
- /^(Status):#{HEADER_RE}$/i,
42
- /^(X-\S+):#{HEADER_RE}$/: header[last = $1] = $2
43
- when /^(Message-Id):#{HEADER_RE}$/i: header[mid_field = last = $1] = $2
44
-
45
- when /^\r*$/: break
46
- when /^\S+:/: last = nil # some other header we don't care about
47
- else
48
- header[last] += " " + line.chomp.gsub(/^\s+/, "") if last
49
- end
50
- end
51
-
52
- if mid_field && header[mid_field] && header[mid_field] =~ /<(.*?)>/
53
- header[mid_field] = $1
8
+ BREAK_RE = /^From \S+ (.+)$/
9
+
10
+ def is_break_line? l
11
+ l =~ BREAK_RE or return false
12
+ time = $1
13
+ begin
14
+ ## hack -- make Time.parse fail when trying to substitute values from Time.now
15
+ Time.parse time, 0
16
+ true
17
+ rescue NoMethodError
18
+ Redwood::log "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
19
+ false
54
20
  end
55
-
56
- header.each do |k, v|
57
- next unless Rfc2047.is_encoded? v
58
- header[k] =
59
- begin
60
- Rfc2047.decode_to $encoding, v
61
- rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
62
- Redwood::log "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
63
- v
64
- end
65
- end
66
- header
67
21
  end
68
-
69
- ## never actually called
70
- def read_body f
71
- body = []
72
- f.each_line do |l|
73
- break if l =~ BREAK_RE
74
- body << l.chomp
75
- end
76
- body
77
- end
78
-
79
- module_function :read_header, :read_body
22
+ module_function :is_break_line?
80
23
  end
81
24
  end
@@ -9,7 +9,7 @@ class Loader < Source
9
9
  attr_accessor :labels
10
10
 
11
11
  ## uri_or_fp is horrific. need to refactor.
12
- def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
12
+ def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=[]
13
13
  @mutex = Mutex.new
14
14
  @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
15
15
 
@@ -56,10 +56,10 @@ class Loader < Source
56
56
  @mutex.synchronize do
57
57
  @f.seek offset
58
58
  l = @f.gets
59
- unless l =~ BREAK_RE
59
+ unless MBox::is_break_line? l
60
60
  raise OutOfSyncSourceError, "mismatch in mbox file offset #{offset.inspect}: #{l.inspect}."
61
61
  end
62
- header = MBox::read_header @f
62
+ header = parse_raw_email_header @f
63
63
  end
64
64
  header
65
65
  end
@@ -68,25 +68,36 @@ class Loader < Source
68
68
  @mutex.synchronize do
69
69
  @f.seek offset
70
70
  begin
71
- RMail::Mailbox::MBoxReader.new(@f).each_message do |input|
72
- m = RMail::Parser.read(input)
73
- if m.body && m.body.is_a?(String)
74
- m.body.gsub!(/^>From /, "From ")
75
- end
76
- return m
77
- end
71
+ ## don't use RMail::Mailbox::MBoxReader because it doesn't properly ignore
72
+ ## "From" at the start of a message body line.
73
+ string = ""
74
+ l = @f.gets
75
+ string << l until @f.eof? || MBox::is_break_line?(l = @f.gets)
76
+ RMail::Parser.read string
78
77
  rescue RMail::Parser::Error => e
79
78
  raise FatalSourceError, "error parsing mbox file: #{e.message}"
80
79
  end
81
80
  end
82
81
  end
83
82
 
83
+ ## scan forward until we're at the valid start of a message
84
+ def correct_offset!
85
+ @mutex.synchronize do
86
+ @f.seek cur_offset
87
+ string = ""
88
+ until @f.eof? || (l = @f.gets) =~ BREAK_RE
89
+ string << l
90
+ end
91
+ self.cur_offset += string.length
92
+ end
93
+ end
94
+
84
95
  def raw_header offset
85
96
  ret = ""
86
97
  @mutex.synchronize do
87
98
  @f.seek offset
88
99
  until @f.eof? || (l = @f.gets) =~ /^\r*$/
89
- ret += l
100
+ ret << l
90
101
  end
91
102
  end
92
103
  ret
@@ -94,7 +105,7 @@ class Loader < Source
94
105
 
95
106
  def raw_message offset
96
107
  ret = ""
97
- each_raw_message_line(offset) { |l| ret += l }
108
+ each_raw_message_line(offset) { |l| ret << l }
98
109
  ret
99
110
  end
100
111
 
@@ -108,7 +119,7 @@ class Loader < Source
108
119
  @mutex.synchronize do
109
120
  @f.seek offset
110
121
  yield @f.gets
111
- until @f.eof? || (l = @f.gets) =~ BREAK_RE
122
+ until @f.eof? || MBox::is_break_line?(l = @f.gets)
112
123
  yield l
113
124
  end
114
125
  end
@@ -129,7 +140,7 @@ class Loader < Source
129
140
  ## 2. at the beginning of an mbox separator (in all other
130
141
  ## cases).
131
142
 
132
- l = @f.gets or raise "next while at EOF"
143
+ l = @f.gets or return nil
133
144
  if l =~ /^\s*$/ # case 1
134
145
  returned_offset = @f.tell
135
146
  @f.gets # now we're at a BREAK_RE, so skip past it
@@ -139,7 +150,7 @@ class Loader < Source
139
150
  end
140
151
 
141
152
  while(line = @f.gets)
142
- break if line =~ BREAK_RE
153
+ break if MBox::is_break_line? line
143
154
  next_offset = @f.tell
144
155
  end
145
156
  end
@@ -45,7 +45,10 @@ module Chunk
45
45
 
46
46
  class Attachment
47
47
  HookManager.register "mime-decode", <<EOS
48
- Executes when decoding a MIME attachment.
48
+ Decodes a MIME attachment into text form. The text will be displayed
49
+ directly in Sup. For attachments that you wish to use a separate program
50
+ to view (e.g. images), you should use the mime-view hook instead.
51
+
49
52
  Variables:
50
53
  content_type: the content-type of the message
51
54
  filename: the filename of the attachment as saved to disk
@@ -57,13 +60,22 @@ Return value:
57
60
  EOS
58
61
 
59
62
  HookManager.register "mime-view", <<EOS
60
- Executes when viewing a MIME attachment, i.e., launching a separate
61
- viewer program.
63
+ Views a non-text MIME attachment. This hook allows you to run
64
+ third-party programs for attachments that require such a thing (e.g.
65
+ images). To instead display a text version of the attachment directly in
66
+ Sup, use the mime-decode hook instead.
67
+
68
+ Note that by default (at least on systems that have a run-mailcap command),
69
+ Sup uses the default mailcap handler for the attachment's MIME type. If
70
+ you want a particular behavior to be global, you may wish to change your
71
+ mailcap instead.
72
+
62
73
  Variables:
63
74
  content_type: the content-type of the attachment
64
75
  filename: the filename of the attachment as saved to disk
65
76
  Return value:
66
- True if the viewing was successful, false otherwise.
77
+ True if the viewing was successful, false otherwise. If false, calling
78
+ /usr/bin/run-mailcap will be tried.
67
79
  EOS
68
80
  #' stupid ruby-mode
69
81
 
@@ -87,7 +99,7 @@ EOS
87
99
  text =
88
100
  case @content_type
89
101
  when /^text\/plain\b/
90
- Message.convert_from @raw_content, encoded_content.charset
102
+ Iconv.easy_decode $encoding, encoded_content.charset || $encoding, @raw_content
91
103
  else
92
104
  HookManager.run "mime-decode", :content_type => content_type,
93
105
  :filename => lambda { write_to_disk },
@@ -108,7 +120,7 @@ EOS
108
120
  if expandable?
109
121
  "Attachment: #{filename} (#{lines.length} lines)"
110
122
  else
111
- "Attachment: #{filename} (#{content_type})"
123
+ "Attachment: #{filename} (#{content_type}; #{@raw_content.size.to_human_size})"
112
124
  end
113
125
  end
114
126
 
@@ -1,10 +1,7 @@
1
1
  require 'time'
2
- require 'iconv'
3
2
 
4
3
  module Redwood
5
4
 
6
- class MessageFormatError < StandardError; end
7
-
8
5
  ## a Message is what's threaded.
9
6
  ##
10
7
  ## it is also where the parsing for quotes and signatures is done, but
@@ -13,8 +10,8 @@ class MessageFormatError < StandardError; end
13
10
  ## specific module that would detect and link to /ruby-talk:\d+/
14
11
  ## sequences in the text of an email. (how sweet would that be?)
15
12
  ##
16
- ## this class cathces all source exceptions. if the underlying source throws
17
- ## an error, it is caught and handled.
13
+ ## this class catches all source exceptions. if the underlying source
14
+ ## throws an error, it is caught and handled.
18
15
 
19
16
  class Message
20
17
  SNIPPET_LEN = 80
@@ -50,7 +47,7 @@ class Message
50
47
  @snippet = opts[:snippet]
51
48
  @snippet_contains_encrypted_content = false
52
49
  @have_snippet = !(opts[:snippet].nil? || opts[:snippet].empty?)
53
- @labels = [] + (opts[:labels] || [])
50
+ @labels = (opts[:labels] || []).to_set_of_symbols
54
51
  @dirty = false
55
52
  @encrypted = false
56
53
  @chunks = nil
@@ -60,54 +57,54 @@ class Message
60
57
  ## why.
61
58
  @refs = []
62
59
 
63
- parse_header(opts[:header] || @source.load_header(@source_info))
60
+ #parse_header(opts[:header] || @source.load_header(@source_info))
64
61
  end
65
62
 
66
63
  def parse_header header
67
- header.keys.each { |k| header[k.downcase] = header[k] } # canonicalize
68
-
69
- fakeid = nil
70
- fakename = nil
71
-
72
- @id =
73
- if header["message-id"]
74
- sanitize_message_id header["message-id"]
75
- else
76
- fakeid = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
77
- end
78
-
79
- @from =
80
- if header["from"]
81
- PersonManager.person_for header["from"]
82
- else
83
- fakename = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
84
- PersonManager.person_for fakename
85
- end
64
+ ## forcibly decode these headers from and to the current encoding,
65
+ ## which serves to strip out characters that aren't displayable
66
+ ## (and which would otherwise be screwing up the display)
67
+ %w(from to subject cc bcc).each do |f|
68
+ header[f] = Iconv.easy_decode($encoding, $encoding, header[f]) if header[f]
69
+ end
86
70
 
87
- Redwood::log "faking message-id for message from #@from: #{id}" if fakeid
88
- Redwood::log "faking from for message #@id: #{fakename}" if fakename
71
+ @id = if header["message-id"]
72
+ mid = header["message-id"] =~ /<(.+?)>/ ? $1 : header["message-id"]
73
+ sanitize_message_id mid
74
+ else
75
+ id = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
76
+ from = header["from"]
77
+ #Redwood::log "faking non-existent message-id for message from #{from}: #{id}"
78
+ id
79
+ end
89
80
 
90
- date = header["date"]
91
- @date =
92
- case date
93
- when Time
94
- date
95
- when String
96
- begin
97
- Time.parse date
98
- rescue ArgumentError => e
99
- Redwood::log "faking date header for #{@id} due to error parsing date #{header['date'].inspect}: #{e.message}"
100
- Time.now
101
- end
102
- else
103
- Redwood::log "faking date header for #{@id}"
81
+ @from = Person.from_address(if header["from"]
82
+ header["from"]
83
+ else
84
+ name = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
85
+ #Redwood::log "faking non-existent sender for message #@id: #{name}"
86
+ name
87
+ end)
88
+
89
+ @date = case(date = header["date"])
90
+ when Time
91
+ date
92
+ when String
93
+ begin
94
+ Time.parse date
95
+ rescue ArgumentError => e
96
+ #Redwood::log "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
104
97
  Time.now
105
98
  end
99
+ else
100
+ #Redwood::log "faking non-existent date header for #{@id}"
101
+ Time.now
102
+ end
106
103
 
107
104
  @subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
108
- @to = PersonManager.people_for header["to"]
109
- @cc = PersonManager.people_for header["cc"]
110
- @bcc = PersonManager.people_for header["bcc"]
105
+ @to = Person.from_address_list header["to"]
106
+ @cc = Person.from_address_list header["cc"]
107
+ @bcc = Person.from_address_list header["bcc"]
111
108
 
112
109
  ## before loading our full header from the source, we can actually
113
110
  ## have some extra refs set by the UI. (this happens when the user
@@ -117,10 +114,10 @@ class Message
117
114
  @refs = (@refs + refs).uniq
118
115
  @replytos = (header["in-reply-to"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
119
116
 
120
- @replyto = PersonManager.person_for header["reply-to"]
117
+ @replyto = Person.from_address header["reply-to"]
121
118
  @list_address =
122
119
  if header["list-post"]
123
- @list_address = PersonManager.person_for header["list-post"].gsub(/^<mailto:|>$/, "")
120
+ @list_address = Person.from_address header["list-post"].gsub(/^<mailto:|>$/, "")
124
121
  else
125
122
  nil
126
123
  end
@@ -130,7 +127,6 @@ class Message
130
127
  @list_subscribe = header["list-subscribe"]
131
128
  @list_unsubscribe = header["list-unsubscribe"]
132
129
  end
133
- private :parse_header
134
130
 
135
131
  def add_ref ref
136
132
  @refs << ref
@@ -172,7 +168,7 @@ class Message
172
168
  def has_label? t; @labels.member? t; end
173
169
  def add_label t
174
170
  return if @labels.member? t
175
- @labels.push t
171
+ @labels = (@labels + [t]).to_set_of_symbols
176
172
  @dirty = true
177
173
  end
178
174
  def remove_label t
@@ -186,7 +182,7 @@ class Message
186
182
  end
187
183
 
188
184
  def labels= l
189
- @labels = l
185
+ @labels = l.to_set_of_symbols
190
186
  @dirty = true
191
187
  end
192
188
 
@@ -198,7 +194,7 @@ class Message
198
194
  ## this is called when the message body needs to actually be loaded.
199
195
  def load_from_source!
200
196
  @chunks ||=
201
- if @source.has_errors?
197
+ if @source.respond_to?(:has_errors?) && @source.has_errors?
202
198
  [Chunk::Text.new(error_message(@source.error.message).split("\n"))]
203
199
  else
204
200
  begin
@@ -212,7 +208,7 @@ class Message
212
208
  ## so i will keep this.
213
209
  parse_header @source.load_header(@source_info)
214
210
  message_to_chunks @source.load_message(@source_info)
215
- rescue SourceError, SocketError, MessageFormatError => e
211
+ rescue SourceError, SocketError => e
216
212
  Redwood::log "problem getting messages from #{@source}: #{e.message}"
217
213
  ## we need force_to_top here otherwise this window will cover
218
214
  ## up the error message one
@@ -372,6 +368,7 @@ private
372
368
  [notice, sig, children].flatten.compact
373
369
  end
374
370
 
371
+ ## takes a RMail::Message, breaks it into Chunk:: classes.
375
372
  def message_to_chunks m, encrypted=false, sibling_types=[]
376
373
  if m.multipart?
377
374
  chunks =
@@ -391,7 +388,7 @@ private
391
388
  elsif m.header.content_type == "message/rfc822"
392
389
  payload = RMail::Parser.read(m.body)
393
390
  from = payload.header.from.first
394
- from_person = from ? PersonManager.person_for(from.format) : nil
391
+ from_person = from ? Person.from_address(from.format) : nil
395
392
  [Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
396
393
  message_to_chunks(payload, encrypted)
397
394
  else
@@ -423,28 +420,20 @@ private
423
420
  # Lowercase the filename because searches are easier that way
424
421
  @attachments.push filename.downcase unless filename =~ /^sup-attachment-/
425
422
  add_label :attachment unless filename =~ /^sup-attachment-/
426
- [Chunk::Attachment.new(m.header.content_type, filename, m, sibling_types)]
423
+ content_type = m.header.content_type || "application/unknown" # sometimes RubyMail gives us nil
424
+ [Chunk::Attachment.new(content_type, filename, m, sibling_types)]
427
425
 
428
426
  ## otherwise, it's body text
429
427
  else
430
- body = Message.convert_from m.decode, m.charset if m.body
428
+ ## if there's no charset, use the current encoding as the charset.
429
+ ## this ensures that the body is normalized to avoid non-displayable
430
+ ## characters
431
+ body = Iconv.easy_decode($encoding, m.charset || $encoding, m.decode) if m.body
431
432
  text_to_chunks((body || "").normalize_whitespace.split("\n"), encrypted)
432
433
  end
433
434
  end
434
435
  end
435
436
 
436
- def self.convert_from body, charset
437
- begin
438
- raise MessageFormatError, "RubyMail decode returned a null body" unless body
439
- return body unless charset
440
- Iconv.easy_decode($encoding, charset, body)
441
- rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence, MessageFormatError => e
442
- Redwood::log "warning: error (#{e.class.name}) decoding message body from #{charset}: #{e.message}"
443
- File.open(File.join(BASE_DIR,"unable-to-decode.txt"), "w") { |f| f.write body }
444
- body
445
- end
446
- end
447
-
448
437
  ## parse the lines of text into chunk objects. the heuristics here
449
438
  ## need tweaking in some nice manner. TODO: move these heuristics
450
439
  ## into the classes themselves.