sup 0.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/History.txt +5 -0
- data/LICENSE +280 -0
- data/Manifest.txt +52 -0
- data/README.txt +119 -0
- data/Rakefile +45 -0
- data/bin/sup +229 -0
- data/bin/sup-import +162 -0
- data/doc/FAQ.txt +38 -0
- data/doc/Philosophy.txt +59 -0
- data/doc/TODO +31 -0
- data/lib/sup.rb +141 -0
- data/lib/sup/account.rb +53 -0
- data/lib/sup/buffer.rb +391 -0
- data/lib/sup/colormap.rb +118 -0
- data/lib/sup/contact.rb +40 -0
- data/lib/sup/draft.rb +105 -0
- data/lib/sup/index.rb +353 -0
- data/lib/sup/keymap.rb +89 -0
- data/lib/sup/label.rb +41 -0
- data/lib/sup/logger.rb +42 -0
- data/lib/sup/mbox.rb +51 -0
- data/lib/sup/mbox/loader.rb +116 -0
- data/lib/sup/message.rb +302 -0
- data/lib/sup/mode.rb +79 -0
- data/lib/sup/modes/buffer-list-mode.rb +37 -0
- data/lib/sup/modes/compose-mode.rb +33 -0
- data/lib/sup/modes/contact-list-mode.rb +121 -0
- data/lib/sup/modes/edit-message-mode.rb +162 -0
- data/lib/sup/modes/forward-mode.rb +38 -0
- data/lib/sup/modes/help-mode.rb +19 -0
- data/lib/sup/modes/inbox-mode.rb +45 -0
- data/lib/sup/modes/label-list-mode.rb +89 -0
- data/lib/sup/modes/label-search-results-mode.rb +29 -0
- data/lib/sup/modes/line-cursor-mode.rb +133 -0
- data/lib/sup/modes/log-mode.rb +44 -0
- data/lib/sup/modes/person-search-results-mode.rb +29 -0
- data/lib/sup/modes/poll-mode.rb +24 -0
- data/lib/sup/modes/reply-mode.rb +136 -0
- data/lib/sup/modes/resume-mode.rb +18 -0
- data/lib/sup/modes/scroll-mode.rb +106 -0
- data/lib/sup/modes/search-results-mode.rb +31 -0
- data/lib/sup/modes/text-mode.rb +51 -0
- data/lib/sup/modes/thread-index-mode.rb +389 -0
- data/lib/sup/modes/thread-view-mode.rb +338 -0
- data/lib/sup/person.rb +120 -0
- data/lib/sup/poll.rb +80 -0
- data/lib/sup/sent.rb +46 -0
- data/lib/sup/tagger.rb +40 -0
- data/lib/sup/textfield.rb +83 -0
- data/lib/sup/thread.rb +358 -0
- data/lib/sup/update.rb +21 -0
- data/lib/sup/util.rb +260 -0
- metadata +123 -0
data/lib/sup/keymap.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "curses"
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
|
5
|
+
class Keymap
|
6
|
+
def initialize
|
7
|
+
@map = {}
|
8
|
+
@order = []
|
9
|
+
yield self if block_given?
|
10
|
+
end
|
11
|
+
|
12
|
+
def keysym_to_keycode k
|
13
|
+
case k
|
14
|
+
when :down: Curses::KEY_DOWN
|
15
|
+
when :up: Curses::KEY_UP
|
16
|
+
when :left: Curses::KEY_LEFT
|
17
|
+
when :right: Curses::KEY_RIGHT
|
18
|
+
when :page_down: Curses::KEY_NPAGE
|
19
|
+
when :page_up: Curses::KEY_PPAGE
|
20
|
+
when :backspace: Curses::KEY_BACKSPACE
|
21
|
+
when :home: Curses::KEY_HOME
|
22
|
+
when :end: Curses::KEY_END
|
23
|
+
when :ctrl_l: "\f"[0]
|
24
|
+
when :ctrl_g: "\a"[0]
|
25
|
+
when :tab: "\t"[0]
|
26
|
+
when :enter, :return: 10 #Curses::KEY_ENTER
|
27
|
+
else
|
28
|
+
if k.is_a?(String) && k.length == 1
|
29
|
+
k[0]
|
30
|
+
else
|
31
|
+
raise ArgumentError, "unknown key name '#{k}'"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def keysym_to_string k
|
37
|
+
case k
|
38
|
+
when :down: "<down arrow>"
|
39
|
+
when :up: "<up arrow>"
|
40
|
+
when :left: "<left arrow>"
|
41
|
+
when :right: "<right arrow>"
|
42
|
+
when :page_down: "<page down>"
|
43
|
+
when :page_up: "<page up>"
|
44
|
+
when :backspace: "<backspace>"
|
45
|
+
when :home: "<home>"
|
46
|
+
when :end: "<end>"
|
47
|
+
when :enter, :return: "<enter>"
|
48
|
+
when :ctrl_l: "ctrl-l"
|
49
|
+
when :ctrl_l: "ctrl-g"
|
50
|
+
when :tab: "tab"
|
51
|
+
when " ": "<space>"
|
52
|
+
else
|
53
|
+
if k.is_a?(String) && k.length == 1
|
54
|
+
k
|
55
|
+
else
|
56
|
+
raise ArgumentError, "unknown key name \"#{k}\""
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def add action, help, *keys
|
62
|
+
entry = [action, help, keys]
|
63
|
+
@order << entry
|
64
|
+
keys.each do |k|
|
65
|
+
raise ArgumentError, "key #{k} already defined (action #{action})" if @map.include? k
|
66
|
+
kc = keysym_to_keycode k
|
67
|
+
@map[kc] = entry
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def action_for kc
|
72
|
+
action, help, keys = @map[kc]
|
73
|
+
action
|
74
|
+
end
|
75
|
+
|
76
|
+
def keysyms; @map.values.map { |action, help, keys| keys }.flatten; end
|
77
|
+
|
78
|
+
def help_text except_for={}
|
79
|
+
lines = @order.map do |action, help, keys|
|
80
|
+
valid_keys = keys.select { |k| !except_for[k] }
|
81
|
+
next if valid_keys.empty?
|
82
|
+
[valid_keys.map { |k| keysym_to_string k }.join(", "), help]
|
83
|
+
end.compact
|
84
|
+
llen = lines.map { |a, b| a.length }.max
|
85
|
+
lines.map { |a, b| sprintf " %#{llen}s : %s", a, b }.join("\n")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
data/lib/sup/label.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class LabelManager
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
## all labels that have special meaning. user will be unable to
|
7
|
+
## add/remove these via normal label mechanisms.
|
8
|
+
RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent ]
|
9
|
+
|
10
|
+
## labels which it nonetheless makes sense to search for by
|
11
|
+
LISTABLE_LABELS = [ :starred, :spam, :draft, :sent ]
|
12
|
+
|
13
|
+
## labels that will never be displayed to the user
|
14
|
+
HIDDEN_LABELS = [ :starred, :unread ]
|
15
|
+
|
16
|
+
def initialize fn
|
17
|
+
@fn = fn
|
18
|
+
labels =
|
19
|
+
if File.exists? fn
|
20
|
+
IO.readlines(fn).map { |x| x.chomp.intern }
|
21
|
+
else
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
@labels = {}
|
25
|
+
labels.each { |t| @labels[t] = true }
|
26
|
+
|
27
|
+
self.class.i_am_the_instance self
|
28
|
+
end
|
29
|
+
|
30
|
+
def user_labels; @labels.keys; end
|
31
|
+
|
32
|
+
def << t; @labels[t] = true unless @labels.member?(t) || RESERVED_LABELS.member?(t); end
|
33
|
+
|
34
|
+
def delete t; @labels.delete t; end
|
35
|
+
|
36
|
+
def save
|
37
|
+
File.open(@fn, "w") { |f| f.puts @labels.keys }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/lib/sup/logger.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class Logger
|
4
|
+
@@instance = nil
|
5
|
+
|
6
|
+
attr_reader :buf
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
raise "only one Log can be defined" if @@instance
|
10
|
+
@@instance = self
|
11
|
+
@mode = LogMode.new
|
12
|
+
@respawn = true
|
13
|
+
@spawning = false # to prevent infinite loops!
|
14
|
+
end
|
15
|
+
|
16
|
+
## must be called if you want to see anything!
|
17
|
+
## once called, will respawn if killed...
|
18
|
+
def make_buf
|
19
|
+
return if @mode.buffer || !BufferManager.instantiated? || !@respawn || @spawning
|
20
|
+
@spawning = true
|
21
|
+
@mode.text = ""
|
22
|
+
@mode.buffer = BufferManager.instance.spawn "<log>", @mode, :hidden => true
|
23
|
+
@spawning = false
|
24
|
+
end
|
25
|
+
|
26
|
+
def log s
|
27
|
+
# $stderr.puts s
|
28
|
+
@mode << "#{Time.now}: #{s}\n"
|
29
|
+
make_buf
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.method_missing m, *a
|
33
|
+
@@instance = Logger.new unless @@instance
|
34
|
+
@@instance.send m, *a
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.buffer
|
38
|
+
@@instance.buf
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/lib/sup/mbox.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require "sup/mbox/loader"
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
|
5
|
+
## some utility functions
|
6
|
+
module MBox
|
7
|
+
BREAK_RE = /^From \S+@\S+/
|
8
|
+
|
9
|
+
def read_header f
|
10
|
+
header = {}
|
11
|
+
last = nil
|
12
|
+
|
13
|
+
## i do it in this weird way because i am trying to speed things up
|
14
|
+
## at load-message time.
|
15
|
+
while(line = f.gets)
|
16
|
+
case line
|
17
|
+
when /^From:\s+(.*)$/i: header[last = "From"] = $1
|
18
|
+
when /^To:\s+(.*)$/i: header[last = "To"] = $1
|
19
|
+
when /^Cc:\s+(.*)$/i: header[last = "Cc"] = $1
|
20
|
+
when /^Bcc:\s+(.*)$/i: header[last = "Bcc"] = $1
|
21
|
+
when /^Subject:\s+(.*)$/i: header[last = "Subject"] = $1
|
22
|
+
when /^Date:\s+(.*)$/i: header[last = "Date"] = $1
|
23
|
+
when /^Message-Id:\s+<(.*)>$/i: header[last = "Message-Id"] = $1
|
24
|
+
when /^References:\s+(.*)$/i: header[last = "References"] = $1
|
25
|
+
when /^In-Reply-To:\s+(.*)$/i: header[last = "In-Reply-To"] = $1
|
26
|
+
when /^List-Post:\s+(.*)$/i: header[last = "List-Post"] = $1
|
27
|
+
when /^Reply-To:\s+(.*)$/i: header[last = "Reply-To"] = $1
|
28
|
+
when /^Status:\s+(.*)$/i: header[last = "Status"] = $1
|
29
|
+
when /^Delivered-To:\s+(.*)$/i
|
30
|
+
header[last = "Delivered-To"] = $1 unless header["Delivered-To"]
|
31
|
+
when /^$/: break
|
32
|
+
when /:/: last = nil
|
33
|
+
else
|
34
|
+
header[last] += line.gsub(/^\s+/, "") if last
|
35
|
+
end
|
36
|
+
end
|
37
|
+
header
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_body f
|
41
|
+
body = []
|
42
|
+
f.each_line do |l|
|
43
|
+
break if l =~ BREAK_RE
|
44
|
+
body << l.chomp
|
45
|
+
end
|
46
|
+
body
|
47
|
+
end
|
48
|
+
|
49
|
+
module_function :read_header, :read_body
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'rmail'
|
3
|
+
|
4
|
+
module Redwood
|
5
|
+
module MBox
|
6
|
+
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
class Loader
|
10
|
+
attr_reader :filename
|
11
|
+
bool_reader :usual, :archived, :read, :dirty
|
12
|
+
attr_accessor :id, :labels
|
13
|
+
|
14
|
+
## end_offset is the last offsets within the file which we've read.
|
15
|
+
## everything after that is considered new messages that haven't
|
16
|
+
## been indexed.
|
17
|
+
def initialize filename, end_offset=0, usual=true, archived=false, id=nil
|
18
|
+
@filename = filename.gsub(%r(^mbox://), "")
|
19
|
+
@end_offset = end_offset
|
20
|
+
@dirty = false
|
21
|
+
@usual = usual
|
22
|
+
@archived = archived
|
23
|
+
@id = id
|
24
|
+
@mutex = Mutex.new
|
25
|
+
@f = File.open @filename
|
26
|
+
@labels = ([
|
27
|
+
:unread,
|
28
|
+
archived ? nil : :inbox,
|
29
|
+
] +
|
30
|
+
if File.dirname(filename) =~ /\b(var|usr|spool)\b/
|
31
|
+
[]
|
32
|
+
else
|
33
|
+
[File.basename(filename).intern]
|
34
|
+
end).compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset!; @end_offset = 0; @dirty = true; end
|
38
|
+
def == o; o.is_a?(Loader) && o.filename == filename; end
|
39
|
+
def to_s; "mbox://#{@filename}"; end
|
40
|
+
|
41
|
+
def is_source_for? s
|
42
|
+
@filename == s || self.to_s == s
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_header offset=nil
|
46
|
+
header = nil
|
47
|
+
@mutex.synchronize do
|
48
|
+
@f.seek offset if offset
|
49
|
+
header = MBox::read_header @f
|
50
|
+
end
|
51
|
+
header
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_message offset
|
55
|
+
ret = nil
|
56
|
+
@mutex.synchronize do
|
57
|
+
@f.seek offset
|
58
|
+
RMail::Mailbox::MBoxReader.new(@f).each_message do |input|
|
59
|
+
return RMail::Parser.read(input)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
## load the full header text
|
65
|
+
def load_header_text offset
|
66
|
+
ret = ""
|
67
|
+
@mutex.synchronize do
|
68
|
+
@f.seek offset
|
69
|
+
until @f.eof? || (l = @f.gets) =~ /^$/
|
70
|
+
ret += l
|
71
|
+
end
|
72
|
+
end
|
73
|
+
ret
|
74
|
+
end
|
75
|
+
|
76
|
+
def next
|
77
|
+
return nil if done?
|
78
|
+
@dirty = true
|
79
|
+
next_end_offset = @end_offset
|
80
|
+
|
81
|
+
@mutex.synchronize do
|
82
|
+
@f.seek @end_offset
|
83
|
+
|
84
|
+
@f.gets # skip the From separator
|
85
|
+
next_end_offset = @f.tell
|
86
|
+
while(line = @f.gets)
|
87
|
+
break if line =~ BREAK_RE
|
88
|
+
next_end_offset = @f.tell + 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
start_offset = @end_offset
|
93
|
+
@end_offset = next_end_offset
|
94
|
+
|
95
|
+
start_offset
|
96
|
+
end
|
97
|
+
|
98
|
+
def each
|
99
|
+
until @end_offset >= File.size(@f)
|
100
|
+
n = self.next
|
101
|
+
yield(n, labels) if n
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def each_header
|
106
|
+
each { |offset, labels| yield offset, labels, load_header(offset) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def done?; @end_offset >= File.size(@f); end
|
110
|
+
def total; File.size @f; end
|
111
|
+
end
|
112
|
+
|
113
|
+
Redwood::register_yaml(Loader, %w(filename end_offset usual archived id))
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
data/lib/sup/message.rb
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Redwood
|
5
|
+
|
6
|
+
class MessageFormatError < StandardError; end
|
7
|
+
|
8
|
+
## a Message is what's threaded.
|
9
|
+
##
|
10
|
+
## it is also where the parsing for quotes and signatures is done, but
|
11
|
+
## that should be moved out to a separate class at some point (because
|
12
|
+
## i would like, for example, to be able to add in a ruby-talk
|
13
|
+
## specific module that would detect and link to /ruby-talk:\d+/
|
14
|
+
## 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
|
+
class Message
|
19
|
+
SNIPPET_LEN = 80
|
20
|
+
RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i
|
21
|
+
|
22
|
+
## some utility methods
|
23
|
+
class << self
|
24
|
+
def normalize_subj s; s.gsub(RE_PATTERN, ""); end
|
25
|
+
def subj_is_reply? s; s =~ RE_PATTERN; end
|
26
|
+
def reify_subj s; subj_is_reply?(s) ? s : "Re: " + s; end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Attachment
|
30
|
+
attr_reader :content_type, :desc
|
31
|
+
def initialize content_type, desc, part
|
32
|
+
@content_type = content_type
|
33
|
+
@desc = desc
|
34
|
+
@part = part
|
35
|
+
@file = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def view!
|
39
|
+
unless @file
|
40
|
+
@file = Tempfile.new "redwood.attachment"
|
41
|
+
@file.print @part.decode
|
42
|
+
@file.close
|
43
|
+
end
|
44
|
+
|
45
|
+
## TODO: handle unknown mime-types
|
46
|
+
system "/usr/bin/run-mailcap --action=view #{@content_type}:#{@file.path}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Text
|
51
|
+
attr_reader :lines
|
52
|
+
def initialize lines
|
53
|
+
## do some wrapping
|
54
|
+
@lines = lines.map { |l| l.wrap 80 }.flatten
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Quote
|
59
|
+
attr_reader :lines
|
60
|
+
def initialize lines
|
61
|
+
@lines = lines
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class Signature
|
66
|
+
attr_reader :lines
|
67
|
+
def initialize lines
|
68
|
+
@lines = lines
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
QUOTE_PATTERN = /^\s{0,4}[>|\}]/
|
73
|
+
BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
|
74
|
+
QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
|
75
|
+
SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)/
|
76
|
+
SIG_DISTANCE = 15 # lines from the end
|
77
|
+
DEFAULT_SUBJECT = "(missing subject)"
|
78
|
+
DEFAULT_SENDER = "(missing sender)"
|
79
|
+
|
80
|
+
attr_reader :id, :date, :from, :subj, :refs, :replytos, :to, :source,
|
81
|
+
:cc, :bcc, :labels, :list_address, :recipient_email, :replyto,
|
82
|
+
:source_info, :mbox_status
|
83
|
+
|
84
|
+
bool_reader :dirty
|
85
|
+
|
86
|
+
def initialize source, source_info, labels, snippet=nil
|
87
|
+
@source = source
|
88
|
+
@source_info = source_info
|
89
|
+
@dirty = false
|
90
|
+
@snippet = snippet
|
91
|
+
@labels = labels
|
92
|
+
|
93
|
+
header = @source.load_header @source_info
|
94
|
+
header.each { |k, v| header[k.downcase] = v }
|
95
|
+
|
96
|
+
%w(message-id date).each do |f|
|
97
|
+
raise MessageFormatError, "no #{f} field in header #{header.inspect} (source #@source offset #@source_info)" unless header.include? f
|
98
|
+
raise MessageFormatError, "nil #{f} field in header #{header.inspect} (source #@source offset #@source_info)" unless header[f]
|
99
|
+
end
|
100
|
+
|
101
|
+
begin
|
102
|
+
@date = Time.parse header["date"]
|
103
|
+
rescue ArgumentError => e
|
104
|
+
raise MessageFormatError, "unparsable date #{header['date']}: #{e.message}"
|
105
|
+
end
|
106
|
+
|
107
|
+
if(@subj = header["subject"])
|
108
|
+
@subj = @subj.gsub(/\s+/, " ").gsub(/\s+$/, "")
|
109
|
+
else
|
110
|
+
@subj = DEFAULT_SUBJECT
|
111
|
+
end
|
112
|
+
@from = Person.for header["from"]
|
113
|
+
@to = Person.for_several header["to"]
|
114
|
+
@cc = Person.for_several header["cc"]
|
115
|
+
@bcc = Person.for_several header["bcc"]
|
116
|
+
@id = header["message-id"]
|
117
|
+
@refs = (header["references"] || "").scan(/<(.*?)>/).flatten
|
118
|
+
@replytos = (header["in-reply-to"] || "").scan(/<(.*?)>/).flatten
|
119
|
+
@replyto = Person.for header["reply-to"]
|
120
|
+
@list_address =
|
121
|
+
if header["list-post"]
|
122
|
+
@list_address = Person.for header["list-post"].gsub(/^<mailto:|>$/, "")
|
123
|
+
else
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
@recipient_email = header["delivered-to"]
|
128
|
+
@mbox_status = header["status"]
|
129
|
+
end
|
130
|
+
|
131
|
+
def snippet
|
132
|
+
to_chunks unless @snippet
|
133
|
+
@snippet
|
134
|
+
end
|
135
|
+
|
136
|
+
def is_list_message?; !@list_address.nil?; end
|
137
|
+
def is_draft?; DraftLoader === @source; end
|
138
|
+
def draft_filename
|
139
|
+
raise "not a draft" unless is_draft?
|
140
|
+
@source.fn_for_offset @source_info
|
141
|
+
end
|
142
|
+
|
143
|
+
def save index
|
144
|
+
index.update_message self if @dirty
|
145
|
+
@dirty = false
|
146
|
+
end
|
147
|
+
|
148
|
+
def has_label? t; @labels.member? t; end
|
149
|
+
def add_label t
|
150
|
+
return if @labels.member? t
|
151
|
+
@labels.push t
|
152
|
+
@dirty = true
|
153
|
+
end
|
154
|
+
def remove_label t
|
155
|
+
return unless @labels.member? t
|
156
|
+
@labels.delete t
|
157
|
+
@dirty = true
|
158
|
+
end
|
159
|
+
|
160
|
+
def recipients
|
161
|
+
@to + @cc + @bcc
|
162
|
+
end
|
163
|
+
|
164
|
+
def labels= l
|
165
|
+
@labels = l
|
166
|
+
@dirty = true
|
167
|
+
end
|
168
|
+
|
169
|
+
def to_chunks
|
170
|
+
m = @source.load_message @source_info
|
171
|
+
message_to_chunks m
|
172
|
+
end
|
173
|
+
|
174
|
+
def header_text
|
175
|
+
@source.load_header_text @source_info
|
176
|
+
end
|
177
|
+
|
178
|
+
def content
|
179
|
+
[
|
180
|
+
from && from.longname,
|
181
|
+
to.map { |p| p.longname },
|
182
|
+
cc.map { |p| p.longname },
|
183
|
+
bcc.map { |p| p.longname },
|
184
|
+
to_chunks.select { |c| c.is_a? Text }.map { |c| c.lines },
|
185
|
+
subj,
|
186
|
+
].flatten.compact.join " "
|
187
|
+
end
|
188
|
+
|
189
|
+
def basic_body_lines
|
190
|
+
to_chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten
|
191
|
+
end
|
192
|
+
|
193
|
+
def basic_header_lines
|
194
|
+
["From: #{@from.full_address}"] +
|
195
|
+
(@to.empty? ? [] : ["To: " + @to.map { |p| p.full_address }.join(", ")]) +
|
196
|
+
(@cc.empty? ? [] : ["Cc: " + @cc.map { |p| p.full_address }.join(", ")]) +
|
197
|
+
(@bcc.empty? ? [] : ["Bcc: " + @bcc.map { |p| p.full_address }.join(", ")]) +
|
198
|
+
["Date: #{@date.rfc822}",
|
199
|
+
"Subject: #{@subj}"]
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
## everything RubyMail-specific goes here.
|
205
|
+
def message_to_chunks m
|
206
|
+
ret = [] <<
|
207
|
+
case m.header.content_type
|
208
|
+
when "text/plain", nil
|
209
|
+
raise MessageFormatError, "no message body before decode" unless
|
210
|
+
m.body
|
211
|
+
body = m.decode or raise MessageFormatError, "no message body"
|
212
|
+
text_to_chunks body.gsub(/\t/, " ").gsub(/\r/, "").split("\n")
|
213
|
+
when "multipart/alternative", "multipart/mixed"
|
214
|
+
nil
|
215
|
+
else
|
216
|
+
disp = m.header["Content-Disposition"] || ""
|
217
|
+
Attachment.new m.header.content_type, disp.gsub(/[\s\n]+/, " "), m
|
218
|
+
end
|
219
|
+
|
220
|
+
m.each_part { |p| ret << message_to_chunks(p) } if m.multipart?
|
221
|
+
ret.compact.flatten
|
222
|
+
end
|
223
|
+
|
224
|
+
## parse the lines of text into chunk objects. the heuristics here
|
225
|
+
## need tweaking in some nice manner. TODO: move these heuristics
|
226
|
+
## into the classes themselves.
|
227
|
+
|
228
|
+
def text_to_chunks lines
|
229
|
+
state = :text # one of :text, :quote, or :sig
|
230
|
+
chunks = []
|
231
|
+
chunk_lines = []
|
232
|
+
|
233
|
+
lines.each_with_index do |line, i|
|
234
|
+
nextline = lines[(i + 1) ... lines.length].find { |l| l !~ /^\s*$/ } # skip blank lines
|
235
|
+
case state
|
236
|
+
when :text
|
237
|
+
newstate = nil
|
238
|
+
if line =~ QUOTE_PATTERN || (line =~ QUOTE_START_PATTERN && (nextline =~ QUOTE_PATTERN || nextline =~ QUOTE_START_PATTERN))
|
239
|
+
newstate = :quote
|
240
|
+
elsif line =~ SIG_PATTERN && (lines.length - i) < SIG_DISTANCE
|
241
|
+
newstate = :sig
|
242
|
+
elsif line =~ BLOCK_QUOTE_PATTERN
|
243
|
+
newstate = :block_quote
|
244
|
+
end
|
245
|
+
if newstate
|
246
|
+
chunks << Text.new(chunk_lines) unless chunk_lines.empty?
|
247
|
+
chunk_lines = [line]
|
248
|
+
state = newstate
|
249
|
+
else
|
250
|
+
chunk_lines << line
|
251
|
+
end
|
252
|
+
when :quote
|
253
|
+
newstate = nil
|
254
|
+
if line =~ QUOTE_PATTERN || line =~ QUOTE_START_PATTERN || line =~ /^\s*$/
|
255
|
+
chunk_lines << line
|
256
|
+
elsif line =~ SIG_PATTERN && (lines.length - i) < SIG_DISTANCE
|
257
|
+
newstate = :sig
|
258
|
+
else
|
259
|
+
newstate = :text
|
260
|
+
end
|
261
|
+
if newstate
|
262
|
+
if chunk_lines.empty?
|
263
|
+
# nothing
|
264
|
+
elsif chunk_lines.size == 1
|
265
|
+
chunks << Text.new(chunk_lines) # forget about one-line quotes
|
266
|
+
else
|
267
|
+
chunks << Quote.new(chunk_lines)
|
268
|
+
end
|
269
|
+
chunk_lines = [line]
|
270
|
+
state = newstate
|
271
|
+
end
|
272
|
+
when :block_quote
|
273
|
+
chunk_lines << line
|
274
|
+
when :sig
|
275
|
+
chunk_lines << line
|
276
|
+
end
|
277
|
+
|
278
|
+
if state == :text && (@snippet.nil? || @snippet.length < SNIPPET_LEN) &&
|
279
|
+
line !~ /[=\*#_-]{3,}/ && line !~ /^\s*$/
|
280
|
+
@snippet = (@snippet ? @snippet + " " : "") + line.gsub(/^\s+/, "").gsub(/[\r\n]/, "").gsub(/\s+/, " ")
|
281
|
+
@snippet = @snippet[0 ... SNIPPET_LEN]
|
282
|
+
end
|
283
|
+
# if @snippet.nil? && state == :text && (line.length > 40 ||
|
284
|
+
# line =~ /\S+.*[^,!:]\s*$/)
|
285
|
+
# @snippet = line.gsub(/^\s+/, "").gsub(/[\r\n]/, "")[0 .. 80]
|
286
|
+
# end
|
287
|
+
end
|
288
|
+
|
289
|
+
## final object
|
290
|
+
case state
|
291
|
+
when :quote, :block_quote
|
292
|
+
chunks << Quote.new(chunk_lines) unless chunk_lines.empty?
|
293
|
+
when :text
|
294
|
+
chunks << Text.new(chunk_lines) unless chunk_lines.empty?
|
295
|
+
when :sig
|
296
|
+
chunks << Signature.new(chunk_lines) unless chunk_lines.empty?
|
297
|
+
end
|
298
|
+
chunks
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|