sup 0.0.8 → 0.1
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.
- data/HACKING +6 -36
- data/History.txt +11 -0
- data/Manifest.txt +5 -0
- data/README.txt +13 -31
- data/Rakefile +3 -3
- data/bin/sup +167 -89
- data/bin/sup-add +39 -29
- data/bin/sup-config +57 -31
- data/bin/sup-sync +60 -54
- data/bin/sup-sync-back +143 -0
- data/doc/FAQ.txt +56 -19
- data/doc/Philosophy.txt +34 -33
- data/doc/TODO +76 -46
- data/doc/UserGuide.txt +142 -122
- data/lib/sup.rb +76 -36
- data/lib/sup/account.rb +27 -19
- data/lib/sup/buffer.rb +130 -44
- data/lib/sup/contact.rb +1 -1
- data/lib/sup/draft.rb +1 -2
- data/lib/sup/imap.rb +64 -19
- data/lib/sup/index.rb +95 -16
- data/lib/sup/keymap.rb +1 -1
- data/lib/sup/label.rb +31 -5
- data/lib/sup/maildir.rb +7 -5
- data/lib/sup/mbox.rb +34 -15
- data/lib/sup/mbox/loader.rb +30 -12
- data/lib/sup/mbox/ssh-loader.rb +7 -5
- data/lib/sup/message.rb +93 -44
- data/lib/sup/modes/buffer-list-mode.rb +1 -1
- data/lib/sup/modes/completion-mode.rb +55 -0
- data/lib/sup/modes/compose-mode.rb +6 -25
- data/lib/sup/modes/contact-list-mode.rb +1 -1
- data/lib/sup/modes/edit-message-mode.rb +119 -29
- data/lib/sup/modes/file-browser-mode.rb +108 -0
- data/lib/sup/modes/forward-mode.rb +3 -20
- data/lib/sup/modes/inbox-mode.rb +9 -12
- data/lib/sup/modes/label-list-mode.rb +28 -46
- data/lib/sup/modes/label-search-results-mode.rb +1 -16
- data/lib/sup/modes/line-cursor-mode.rb +44 -5
- data/lib/sup/modes/person-search-results-mode.rb +1 -16
- data/lib/sup/modes/reply-mode.rb +18 -31
- data/lib/sup/modes/resume-mode.rb +6 -6
- data/lib/sup/modes/scroll-mode.rb +6 -5
- data/lib/sup/modes/search-results-mode.rb +6 -17
- data/lib/sup/modes/thread-index-mode.rb +70 -28
- data/lib/sup/modes/thread-view-mode.rb +65 -29
- data/lib/sup/person.rb +71 -30
- data/lib/sup/poll.rb +13 -4
- data/lib/sup/rfc2047.rb +61 -0
- data/lib/sup/sent.rb +7 -5
- data/lib/sup/source.rb +12 -9
- data/lib/sup/suicide.rb +36 -0
- data/lib/sup/tagger.rb +6 -6
- data/lib/sup/textfield.rb +76 -14
- data/lib/sup/thread.rb +97 -123
- data/lib/sup/util.rb +167 -1
- metadata +30 -5
data/lib/sup/maildir.rb
CHANGED
@@ -11,20 +11,24 @@ module Redwood
|
|
11
11
|
class Maildir < Source
|
12
12
|
SCAN_INTERVAL = 30 # seconds
|
13
13
|
|
14
|
-
|
15
|
-
|
14
|
+
yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
|
15
|
+
def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[]
|
16
|
+
super uri, last_date, usual, archived, id
|
16
17
|
uri = URI(uri)
|
17
18
|
|
18
19
|
raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
|
19
20
|
raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
|
20
21
|
|
21
22
|
@dir = uri.path
|
23
|
+
@labels = (labels || []).freeze
|
22
24
|
@ids = []
|
23
25
|
@ids_to_fns = {}
|
24
26
|
@last_scan = nil
|
25
27
|
@mutex = Mutex.new
|
26
28
|
end
|
27
29
|
|
30
|
+
def self.suggest_labels_for path; [] end
|
31
|
+
|
28
32
|
def check
|
29
33
|
scan_mailbox
|
30
34
|
start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
|
@@ -89,7 +93,7 @@ class Maildir < Source
|
|
89
93
|
start.upto(@ids.length - 1) do |i|
|
90
94
|
id = @ids[i]
|
91
95
|
self.cur_offset = id
|
92
|
-
yield id, (@ids_to_fns[id] =~ /,.*R.*$/ ? [] : [:unread])
|
96
|
+
yield id, @labels + (@ids_to_fns[id] =~ /,.*R.*$/ ? [] : [:unread])
|
93
97
|
end
|
94
98
|
end
|
95
99
|
|
@@ -122,6 +126,4 @@ private
|
|
122
126
|
end
|
123
127
|
end
|
124
128
|
|
125
|
-
Redwood::register_yaml(Maildir, %w(uri cur_offset usual archived id))
|
126
|
-
|
127
129
|
end
|
data/lib/sup/mbox.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
require "sup/mbox/loader"
|
2
2
|
require "sup/mbox/ssh-file"
|
3
3
|
require "sup/mbox/ssh-loader"
|
4
|
+
require "sup/rfc2047"
|
4
5
|
|
5
6
|
module Redwood
|
6
7
|
|
7
|
-
## some utility functions
|
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
|
8
12
|
module MBox
|
9
13
|
BREAK_RE = /^From \S+/
|
10
14
|
|
@@ -16,31 +20,46 @@ module MBox
|
|
16
20
|
## when scanning over large mbox files.
|
17
21
|
while(line = f.gets)
|
18
22
|
case line
|
19
|
-
when /^(From):\s+(
|
20
|
-
/^(To):\s+(
|
21
|
-
/^(Cc):\s+(
|
22
|
-
/^(Bcc):\s+(
|
23
|
-
/^(Subject):\s+(
|
24
|
-
/^(Date):\s+(
|
25
|
-
/^(
|
26
|
-
/^(
|
27
|
-
/^(
|
28
|
-
/^(
|
29
|
-
/^(
|
30
|
-
|
23
|
+
when /^(From):\s+(.*?)\s*$/i,
|
24
|
+
/^(To):\s+(.*?)\s*$/i,
|
25
|
+
/^(Cc):\s+(.*?)\s*$/i,
|
26
|
+
/^(Bcc):\s+(.*?)\s*$/i,
|
27
|
+
/^(Subject):\s+(.*?)\s*$/i,
|
28
|
+
/^(Date):\s+(.*?)\s*$/i,
|
29
|
+
/^(References):\s+(.*?)\s*$/i,
|
30
|
+
/^(In-Reply-To):\s+(.*?)\s*$/i,
|
31
|
+
/^(Reply-To):\s+(.*?)\s*$/i,
|
32
|
+
/^(List-Post):\s+(.*?)\s*$/i,
|
33
|
+
/^(Status):\s+(.*?)\s*$/i: header[last = $1] = $2
|
34
|
+
when /^(Message-Id):\s+(.*?)\s*$/i: header[mid_field = last = $1] = $2
|
31
35
|
|
32
36
|
## these next three can occur multiple times, and we want the
|
33
37
|
## first one
|
34
38
|
when /^(Delivered-To):\s+(.*)$/i,
|
35
39
|
/^(X-Original-To):\s+(.*)$/i,
|
36
|
-
/^(Envelope-To):\s+(.*)$/i: header[last = $1
|
40
|
+
/^(Envelope-To):\s+(.*)$/i: header[last = $1] ||= $2
|
37
41
|
|
38
42
|
when /^$/: break
|
39
|
-
when /:/: last = nil
|
43
|
+
when /:/: last = nil # some other header we don't care about
|
40
44
|
else
|
41
45
|
header[last] += " " + line.chomp.gsub(/^\s+/, "") if last
|
42
46
|
end
|
43
47
|
end
|
48
|
+
|
49
|
+
if mid_field && header[mid_field] && header[mid_field] =~ /<(.*?)>/
|
50
|
+
header[mid_field] = $1
|
51
|
+
end
|
52
|
+
|
53
|
+
header.each do |k, v|
|
54
|
+
next unless Rfc2047.is_encoded? v
|
55
|
+
header[k] =
|
56
|
+
begin
|
57
|
+
Rfc2047.decode_to $encoding, v
|
58
|
+
rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
|
59
|
+
Redwood::log "warning: error decoding RFC 2047 header: #{e.message}"
|
60
|
+
v
|
61
|
+
end
|
62
|
+
end
|
44
63
|
header
|
45
64
|
end
|
46
65
|
|
data/lib/sup/mbox/loader.rb
CHANGED
@@ -5,26 +5,36 @@ module Redwood
|
|
5
5
|
module MBox
|
6
6
|
|
7
7
|
class Loader < Source
|
8
|
-
|
9
|
-
|
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
|
10
11
|
|
11
12
|
@mutex = Mutex.new
|
12
|
-
@labels = [
|
13
|
+
@labels = (labels || []).freeze
|
13
14
|
|
14
15
|
case uri_or_fp
|
15
16
|
when String
|
16
17
|
uri = URI(uri_or_fp)
|
17
18
|
raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
|
18
19
|
raise ArgumentError, "mbox uri ('#{uri}') cannot have a host: #{uri.host}" if uri.host
|
19
|
-
## heuristic: use the filename as a label, unless the file
|
20
|
-
## has a path that probably represents an inbox.
|
21
|
-
@labels << File.basename(uri.path).intern unless File.dirname(uri.path) =~ /\b(var|usr|spool)\b/
|
22
20
|
@f = File.open uri.path
|
23
21
|
else
|
24
22
|
@f = uri_or_fp
|
25
23
|
end
|
26
24
|
end
|
27
25
|
|
26
|
+
def file_path; URI(uri).path end
|
27
|
+
|
28
|
+
def self.suggest_labels_for path
|
29
|
+
## heuristic: use the filename as a label, unless the file
|
30
|
+
## has a path that probably represents an inbox.
|
31
|
+
if File.dirname(path) =~ /\b(var|usr|spool)\b/
|
32
|
+
[]
|
33
|
+
else
|
34
|
+
[File.basename(path).intern]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
28
38
|
def check
|
29
39
|
if (cur_offset ||= start_offset) > end_offset
|
30
40
|
raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
|
@@ -73,14 +83,24 @@ class Loader < Source
|
|
73
83
|
|
74
84
|
def raw_full_message offset
|
75
85
|
ret = ""
|
86
|
+
each_raw_full_message_line(offset) { |l| ret += l }
|
87
|
+
ret
|
88
|
+
end
|
89
|
+
|
90
|
+
## apparently it's a million times faster to call this directly if
|
91
|
+
## we're just moving messages around on disk, than reading things
|
92
|
+
## into memory with raw_full_message.
|
93
|
+
##
|
94
|
+
## i hoped never to have to move shit around on disk but
|
95
|
+
## sup-sync-back has to do it.
|
96
|
+
def each_raw_full_message_line offset
|
76
97
|
@mutex.synchronize do
|
77
98
|
@f.seek offset
|
78
|
-
@f.gets
|
99
|
+
yield @f.gets
|
79
100
|
until @f.eof? || (l = @f.gets) =~ BREAK_RE
|
80
|
-
|
101
|
+
yield l
|
81
102
|
end
|
82
103
|
end
|
83
|
-
ret
|
84
104
|
end
|
85
105
|
|
86
106
|
def next
|
@@ -117,11 +137,9 @@ class Loader < Source
|
|
117
137
|
end
|
118
138
|
|
119
139
|
self.cur_offset = next_offset
|
120
|
-
[returned_offset, @labels
|
140
|
+
[returned_offset, @labels]
|
121
141
|
end
|
122
142
|
end
|
123
143
|
|
124
|
-
Redwood::register_yaml(Loader, %w(uri cur_offset usual archived id))
|
125
|
-
|
126
144
|
end
|
127
145
|
end
|
data/lib/sup/mbox/ssh-loader.rb
CHANGED
@@ -6,7 +6,10 @@ module MBox
|
|
6
6
|
class SSHLoader < Source
|
7
7
|
attr_accessor :username, :password
|
8
8
|
|
9
|
-
|
9
|
+
yaml_properties :uri, :username, :password, :cur_offset, :usual,
|
10
|
+
:archived, :id, :labels
|
11
|
+
|
12
|
+
def initialize uri, username=nil, password=nil, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
|
10
13
|
raise ArgumentError, "not an mbox+ssh uri: #{uri.inspect}" unless uri =~ %r!^mbox\+ssh://!
|
11
14
|
|
12
15
|
super uri, start_offset, usual, archived, id
|
@@ -16,6 +19,7 @@ class SSHLoader < Source
|
|
16
19
|
@password = password
|
17
20
|
@uri = uri
|
18
21
|
@cur_offset = start_offset
|
22
|
+
@labels = (labels || []).freeze
|
19
23
|
|
20
24
|
opts = {}
|
21
25
|
opts[:username] = @username if @username
|
@@ -26,10 +30,10 @@ class SSHLoader < Source
|
|
26
30
|
|
27
31
|
## heuristic: use the filename as a label, unless the file
|
28
32
|
## has a path that probably represents an inbox.
|
29
|
-
@labels = [:unread]
|
30
|
-
@labels << File.basename(filename).intern unless File.dirname(filename) =~ /\b(var|usr|spool)\b/
|
31
33
|
end
|
32
34
|
|
35
|
+
def self.suggest_labels_for path; Loader.suggest_labels_for(path) end
|
36
|
+
|
33
37
|
def connect; safely { @f.connect }; end
|
34
38
|
def host; @parsed_uri.host; end
|
35
39
|
def filename; @parsed_uri.path[1..-1] end
|
@@ -66,7 +70,5 @@ class SSHLoader < Source
|
|
66
70
|
end
|
67
71
|
end
|
68
72
|
|
69
|
-
Redwood::register_yaml(SSHLoader, %w(uri username password cur_offset usual archived id))
|
70
|
-
|
71
73
|
end
|
72
74
|
end
|
data/lib/sup/message.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'tempfile'
|
2
2
|
require 'time'
|
3
|
+
require 'iconv'
|
3
4
|
|
4
5
|
module Redwood
|
5
6
|
|
@@ -12,9 +13,6 @@ class MessageFormatError < StandardError; end
|
|
12
13
|
## i would like, for example, to be able to add in a ruby-talk
|
13
14
|
## specific module that would detect and link to /ruby-talk:\d+/
|
14
15
|
## sequences in the text of an email. (how sweet would that be?)
|
15
|
-
##
|
16
|
-
## TODO: integrate with user's addressbook to render names
|
17
|
-
## appropriately.
|
18
16
|
class Message
|
19
17
|
SNIPPET_LEN = 80
|
20
18
|
WRAP_LEN = 80 # wrap at this width
|
@@ -28,28 +26,30 @@ class Message
|
|
28
26
|
end
|
29
27
|
|
30
28
|
class Attachment
|
31
|
-
attr_reader :content_type, :
|
32
|
-
def initialize content_type,
|
29
|
+
attr_reader :content_type, :filename, :content, :lines
|
30
|
+
def initialize content_type, filename, content
|
33
31
|
@content_type = content_type
|
34
|
-
@
|
35
|
-
@
|
36
|
-
|
37
|
-
|
32
|
+
@filename = filename
|
33
|
+
@content = content
|
34
|
+
|
35
|
+
if inlineable?
|
36
|
+
@lines = to_s.split("\n")
|
37
|
+
end
|
38
38
|
end
|
39
39
|
|
40
40
|
def view!
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@file.close
|
45
|
-
end
|
41
|
+
file = Tempfile.new "redwood.attachment"
|
42
|
+
file.print raw_content
|
43
|
+
file.close
|
46
44
|
|
47
|
-
|
48
|
-
system "/usr/bin/run-mailcap --action=view #{@content_type}:#{@file.path}"
|
45
|
+
system "/usr/bin/run-mailcap --action=view #{@content_type}:#{file.path} >& /dev/null"
|
49
46
|
$? == 0
|
50
47
|
end
|
51
48
|
|
52
|
-
def to_s; @
|
49
|
+
def to_s; Message.decode_and_convert @content; end
|
50
|
+
def raw_content; @content.decode end
|
51
|
+
|
52
|
+
def inlineable?; @content_type =~ /^text\/plain/ end
|
53
53
|
end
|
54
54
|
|
55
55
|
class Text
|
@@ -74,10 +74,12 @@ class Message
|
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
|
+
|
77
78
|
QUOTE_PATTERN = /^\s{0,4}[>|\}]/
|
78
79
|
BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
|
79
80
|
QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
|
80
81
|
SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)|(^\s*--~--~-)/
|
82
|
+
|
81
83
|
MAX_SIG_DISTANCE = 15 # lines from the end
|
82
84
|
DEFAULT_SUBJECT = "(missing subject)"
|
83
85
|
DEFAULT_SENDER = "(missing sender)"
|
@@ -95,7 +97,7 @@ class Message
|
|
95
97
|
@source_info = opts[:source_info] or raise ArgumentError, "source_info can't be nil"
|
96
98
|
@snippet = opts[:snippet] || ""
|
97
99
|
@have_snippet = !opts[:snippet].nil?
|
98
|
-
@labels = opts[:labels] || []
|
100
|
+
@labels = [] + (opts[:labels] || [])
|
99
101
|
@dirty = false
|
100
102
|
@chunks = nil
|
101
103
|
|
@@ -118,17 +120,17 @@ class Message
|
|
118
120
|
end
|
119
121
|
|
120
122
|
@subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
|
121
|
-
@from =
|
122
|
-
@to =
|
123
|
-
@cc =
|
124
|
-
@bcc =
|
123
|
+
@from = PersonManager.person_for header["from"]
|
124
|
+
@to = PersonManager.people_for header["to"]
|
125
|
+
@cc = PersonManager.people_for header["cc"]
|
126
|
+
@bcc = PersonManager.people_for header["bcc"]
|
125
127
|
@id = header["message-id"]
|
126
128
|
@refs = (header["references"] || "").gsub(/[<>]/, "").split(/\s+/).flatten
|
127
129
|
@replytos = (header["in-reply-to"] || "").scan(/<(.*?)>/).flatten
|
128
|
-
@replyto =
|
130
|
+
@replyto = PersonManager.person_for header["reply-to"]
|
129
131
|
@list_address =
|
130
132
|
if header["list-post"]
|
131
|
-
@list_address =
|
133
|
+
@list_address = PersonManager.person_for header["list-post"].gsub(/^<mailto:|>$/, "")
|
132
134
|
else
|
133
135
|
nil
|
134
136
|
end
|
@@ -140,7 +142,7 @@ class Message
|
|
140
142
|
|
141
143
|
def snippet; @snippet || chunks && @snippet; end
|
142
144
|
def is_list_message?; !@list_address.nil?; end
|
143
|
-
def is_draft?;
|
145
|
+
def is_draft?; @source.is_a? DraftLoader; end
|
144
146
|
def draft_filename
|
145
147
|
raise "not a draft" unless is_draft?
|
146
148
|
@source.fn_for_offset @source_info
|
@@ -190,6 +192,7 @@ class Message
|
|
190
192
|
read_header @source.load_header(@source_info)
|
191
193
|
message_to_chunks @source.load_message(@source_info)
|
192
194
|
rescue SourceError, SocketError, MessageFormatError => e
|
195
|
+
Redwood::log "problem getting messages from #{@source}: #{e.message}"
|
193
196
|
## we need force_to_top here otherwise this window will cover
|
194
197
|
## up the error message one
|
195
198
|
Redwood::report_broken_sources :force_to_top => true
|
@@ -221,6 +224,7 @@ EOS
|
|
221
224
|
begin
|
222
225
|
@source.raw_header @source_info
|
223
226
|
rescue SourceError => e
|
227
|
+
Redwood::log "problem getting messages from #{@source}: #{e.message}"
|
224
228
|
error_message e.message
|
225
229
|
end
|
226
230
|
end
|
@@ -229,6 +233,7 @@ EOS
|
|
229
233
|
begin
|
230
234
|
@source.raw_full_message @source_info
|
231
235
|
rescue SourceError => e
|
236
|
+
Redwood::log "problem getting messages from #{@source}: #{e.message}"
|
232
237
|
error_message(e.message)
|
233
238
|
end
|
234
239
|
end
|
@@ -260,22 +265,71 @@ EOS
|
|
260
265
|
|
261
266
|
private
|
262
267
|
|
263
|
-
##
|
268
|
+
## here's where we handle decoding mime attachments. unfortunately
|
269
|
+
## but unsurprisingly, the world of mime attachments is a bit of a
|
270
|
+
## mess. as an empiricist, i'm basing the following behavior on
|
271
|
+
## observed mail rather than on interpretations of rfcs, so probably
|
272
|
+
## this will have to be tweaked.
|
273
|
+
##
|
274
|
+
## the general behavior i want is: ignore content-disposition, at
|
275
|
+
## least in so far as it suggests something being inline vs being an
|
276
|
+
## attachment. (because really, that should be the recipient's
|
277
|
+
## decision to make.) if a mime part is text/plain, then decode it
|
278
|
+
## and display it inline. if it has associated filename, then make
|
279
|
+
## it collapsable and individually saveable; otherwise, treat it as
|
280
|
+
## regular body text.
|
281
|
+
##
|
282
|
+
## so, in contrast to mutt, the user is not exposed to the workings
|
283
|
+
## of the gruesome slaughterhouse and sausage factory that is a
|
284
|
+
## mime-encoded message, but need only see the delicious end
|
285
|
+
## product.
|
264
286
|
def message_to_chunks m
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
287
|
+
if m.multipart?
|
288
|
+
m.body.map { |p| message_to_chunks p }.flatten.compact # recurse
|
289
|
+
else
|
290
|
+
filename =
|
291
|
+
## first, paw through the headers looking for a filename
|
292
|
+
if m.header["Content-Disposition"] &&
|
293
|
+
m.header["Content-Disposition"] =~ /filename="?(.*?[^\\])("|;|$)/
|
294
|
+
$1
|
295
|
+
elsif m.header["Content-Type"] &&
|
296
|
+
m.header["Content-Type"] =~ /name=(.*?)(;|$)/
|
297
|
+
$1
|
298
|
+
|
299
|
+
## haven't found one, but it's a non-text message. fake
|
300
|
+
## it.
|
301
|
+
elsif m.header["Content-Type"] && m.header["Content-Type"] !~ /^text\/plain/
|
302
|
+
"sup-attachment-#{Time.now.to_i}-#{rand 10000}"
|
303
|
+
end
|
304
|
+
|
305
|
+
## if there's a filename, we'll treat it as an attachment.
|
306
|
+
if filename
|
307
|
+
[Attachment.new(m.header.content_type, filename, m)]
|
308
|
+
|
309
|
+
## otherwise, it's body text
|
272
310
|
else
|
273
|
-
|
274
|
-
|
311
|
+
body = Message.decode_and_convert m
|
312
|
+
text_to_chunks body.normalize_whitespace.split("\n")
|
275
313
|
end
|
276
|
-
|
277
|
-
|
278
|
-
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def self.decode_and_convert m
|
318
|
+
charset =
|
319
|
+
if m.header.field?("content-type") && m.header.fetch("content-type") =~ /charset=(.*?)(;|$)/
|
320
|
+
$1
|
321
|
+
end
|
322
|
+
|
323
|
+
m.body && body = m.decode or raise MessageFormatError, "For some bizarre reason, RubyMail was unable to parse this message."
|
324
|
+
|
325
|
+
if charset
|
326
|
+
begin
|
327
|
+
body = Iconv.iconv($encoding, charset, body).join
|
328
|
+
rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
|
329
|
+
Redwood::log "warning: error decoding message body from #{charset}: #{e.message}"
|
330
|
+
end
|
331
|
+
end
|
332
|
+
body
|
279
333
|
end
|
280
334
|
|
281
335
|
## parse the lines of text into chunk objects. the heuristics here
|
@@ -323,8 +377,6 @@ private
|
|
323
377
|
if newstate
|
324
378
|
if chunk_lines.empty?
|
325
379
|
# nothing
|
326
|
-
elsif chunk_lines.size == 1
|
327
|
-
chunks << Text.new(chunk_lines) # forget about one-line quotes
|
328
380
|
else
|
329
381
|
chunks << Quote.new(chunk_lines)
|
330
382
|
end
|
@@ -332,10 +384,7 @@ private
|
|
332
384
|
state = newstate
|
333
385
|
end
|
334
386
|
|
335
|
-
when :block_quote
|
336
|
-
chunk_lines << line
|
337
|
-
|
338
|
-
when :sig
|
387
|
+
when :block_quote, :sig
|
339
388
|
chunk_lines << line
|
340
389
|
end
|
341
390
|
|