sup 0.0.1 → 0.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.
- data/History.txt +7 -0
- data/Manifest.txt +2 -0
- data/README.txt +25 -21
- data/bin/sup +29 -25
- data/bin/sup-import +43 -13
- data/doc/FAQ.txt +8 -6
- data/doc/Philosophy.txt +17 -18
- data/doc/TODO +6 -0
- data/lib/sup.rb +7 -6
- data/lib/sup/buffer.rb +28 -17
- data/lib/sup/draft.rb +27 -23
- data/lib/sup/imap.rb +107 -0
- data/lib/sup/index.rb +47 -55
- data/lib/sup/logger.rb +1 -2
- data/lib/sup/mbox.rb +2 -2
- data/lib/sup/mbox/loader.rb +63 -66
- data/lib/sup/message.rb +45 -25
- data/lib/sup/mode.rb +1 -0
- data/lib/sup/modes/buffer-list-mode.rb +4 -4
- data/lib/sup/modes/edit-message-mode.rb +4 -4
- data/lib/sup/modes/inbox-mode.rb +2 -0
- data/lib/sup/modes/label-list-mode.rb +1 -1
- data/lib/sup/modes/log-mode.rb +1 -1
- data/lib/sup/modes/poll-mode.rb +2 -2
- data/lib/sup/modes/reply-mode.rb +6 -3
- data/lib/sup/modes/resume-mode.rb +25 -0
- data/lib/sup/modes/scroll-mode.rb +7 -0
- data/lib/sup/modes/thread-index-mode.rb +8 -11
- data/lib/sup/modes/thread-view-mode.rb +31 -3
- data/lib/sup/poll.rb +24 -10
- data/lib/sup/sent.rb +8 -8
- data/lib/sup/source.rb +64 -0
- data/lib/sup/thread.rb +3 -0
- data/lib/sup/util.rb +2 -2
- metadata +4 -2
data/lib/sup/logger.rb
CHANGED
@@ -18,14 +18,13 @@ class Logger
|
|
18
18
|
def make_buf
|
19
19
|
return if @mode.buffer || !BufferManager.instantiated? || !@respawn || @spawning
|
20
20
|
@spawning = true
|
21
|
-
@mode.text = ""
|
22
21
|
@mode.buffer = BufferManager.instance.spawn "<log>", @mode, :hidden => true
|
23
22
|
@spawning = false
|
24
23
|
end
|
25
24
|
|
26
25
|
def log s
|
27
26
|
# $stderr.puts s
|
28
|
-
@mode << "#{Time.now}: #{s}\n"
|
27
|
+
@mode << "#{Time.now}: #{s.chomp}\n"
|
29
28
|
make_buf
|
30
29
|
end
|
31
30
|
|
data/lib/sup/mbox.rb
CHANGED
@@ -4,14 +4,14 @@ module Redwood
|
|
4
4
|
|
5
5
|
## some utility functions
|
6
6
|
module MBox
|
7
|
-
BREAK_RE = /^From \S
|
7
|
+
BREAK_RE = /^From \S+/
|
8
8
|
|
9
9
|
def read_header f
|
10
10
|
header = {}
|
11
11
|
last = nil
|
12
12
|
|
13
13
|
## i do it in this weird way because i am trying to speed things up
|
14
|
-
##
|
14
|
+
## when scanning over large mbox files.
|
15
15
|
while(line = f.gets)
|
16
16
|
case line
|
17
17
|
when /^From:\s+(.*)$/i: header[last = "From"] = $1
|
data/lib/sup/mbox/loader.rb
CHANGED
@@ -4,65 +4,53 @@ require 'rmail'
|
|
4
4
|
module Redwood
|
5
5
|
module MBox
|
6
6
|
|
7
|
-
class
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
7
|
+
class Loader < Source
|
8
|
+
attr_reader :labels
|
9
|
+
|
10
|
+
def initialize uri, start_offset=nil, usual=true, archived=false, id=nil
|
11
|
+
raise ArgumentError, "not an mbox uri" unless uri =~ %r!mbox://!
|
12
|
+
super
|
13
|
+
|
24
14
|
@mutex = Mutex.new
|
15
|
+
@filename = uri.sub(%r!^mbox://!, "")
|
25
16
|
@f = File.open @filename
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
if File.dirname(filename) =~ /\b(var|usr|spool)\b/
|
31
|
-
[]
|
32
|
-
else
|
33
|
-
[File.basename(filename).intern]
|
34
|
-
end).compact
|
17
|
+
## heuristic: use the filename as a label, unless the file
|
18
|
+
## has a path that probably represents an inbox.
|
19
|
+
@labels = [:unread]
|
20
|
+
@labels << File.basename(@filename).intern unless File.dirname(@filename) =~ /\b(var|usr|spool)\b/
|
35
21
|
end
|
36
22
|
|
37
|
-
def
|
38
|
-
def
|
39
|
-
def to_s; "mbox://#{@filename}"; end
|
40
|
-
|
41
|
-
def is_source_for? s
|
42
|
-
@filename == s || self.to_s == s
|
43
|
-
end
|
23
|
+
def start_offset; 0; end
|
24
|
+
def end_offset; File.size @f; end
|
44
25
|
|
45
|
-
def load_header offset
|
26
|
+
def load_header offset
|
46
27
|
header = nil
|
47
28
|
@mutex.synchronize do
|
48
|
-
@f.seek offset
|
29
|
+
@f.seek offset
|
30
|
+
l = @f.gets
|
31
|
+
unless l =~ BREAK_RE
|
32
|
+
self.broken_msg = "offset mismatch in mbox file offset #{offset.inspect}: #{l.inspect}. Run 'sup-import --rebuild #{to_s}' to correct this."
|
33
|
+
raise SourceError, self.broken_msg
|
34
|
+
end
|
49
35
|
header = MBox::read_header @f
|
50
36
|
end
|
51
37
|
header
|
52
38
|
end
|
53
39
|
|
54
40
|
def load_message offset
|
55
|
-
ret = nil
|
56
41
|
@mutex.synchronize do
|
57
42
|
@f.seek offset
|
58
|
-
|
59
|
-
|
43
|
+
begin
|
44
|
+
RMail::Mailbox::MBoxReader.new(@f).each_message do |input|
|
45
|
+
return RMail::Parser.read(input)
|
46
|
+
end
|
47
|
+
rescue RMail::Parser::Error => e
|
48
|
+
raise SourceError, "error parsing message with rmail: #{e.message}"
|
60
49
|
end
|
61
50
|
end
|
62
51
|
end
|
63
52
|
|
64
|
-
|
65
|
-
def load_header_text offset
|
53
|
+
def raw_header offset
|
66
54
|
ret = ""
|
67
55
|
@mutex.synchronize do
|
68
56
|
@f.seek offset
|
@@ -73,44 +61,53 @@ class Loader
|
|
73
61
|
ret
|
74
62
|
end
|
75
63
|
|
64
|
+
def raw_full_message offset
|
65
|
+
ret = ""
|
66
|
+
@mutex.synchronize do
|
67
|
+
@f.seek offset
|
68
|
+
@f.gets # skip mbox header
|
69
|
+
until @f.eof? || (l = @f.gets) =~ BREAK_RE
|
70
|
+
ret += l
|
71
|
+
end
|
72
|
+
end
|
73
|
+
ret
|
74
|
+
end
|
75
|
+
|
76
76
|
def next
|
77
|
-
|
78
|
-
|
79
|
-
next_end_offset = @end_offset
|
77
|
+
returned_offset = nil
|
78
|
+
next_offset = cur_offset
|
80
79
|
|
81
80
|
@mutex.synchronize do
|
82
|
-
@f.seek
|
81
|
+
@f.seek cur_offset
|
82
|
+
|
83
|
+
## cur_offset could be at one of two places here:
|
84
|
+
|
85
|
+
## 1. before a \n and a mbox separator, if it was previously at
|
86
|
+
## EOF and a new message was added; or,
|
87
|
+
## 2. at the beginning of an mbox separator (in all other
|
88
|
+
## cases).
|
89
|
+
|
90
|
+
l = @f.gets or raise "next while at EOF"
|
91
|
+
if l =~ /^\s*$/ # case 1
|
92
|
+
returned_offset = @f.tell
|
93
|
+
@f.gets # now we're at a BREAK_RE, so skip past it
|
94
|
+
else # case 2
|
95
|
+
returned_offset = cur_offset
|
96
|
+
## we've already skipped past the BREAK_RE, to just go
|
97
|
+
end
|
83
98
|
|
84
|
-
@f.gets # skip the From separator
|
85
|
-
next_end_offset = @f.tell
|
86
99
|
while(line = @f.gets)
|
87
100
|
break if line =~ BREAK_RE
|
88
|
-
|
101
|
+
next_offset = @f.tell
|
89
102
|
end
|
90
103
|
end
|
91
104
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
start_offset
|
105
|
+
self.cur_offset = next_offset
|
106
|
+
[returned_offset, labels]
|
96
107
|
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
108
|
end
|
112
109
|
|
113
|
-
Redwood::register_yaml(Loader, %w(
|
110
|
+
Redwood::register_yaml(Loader, %w(uri cur_offset usual archived id))
|
114
111
|
|
115
112
|
end
|
116
113
|
end
|
data/lib/sup/message.rb
CHANGED
@@ -27,24 +27,27 @@ class Message
|
|
27
27
|
end
|
28
28
|
|
29
29
|
class Attachment
|
30
|
-
attr_reader :content_type, :desc
|
30
|
+
attr_reader :content_type, :desc, :filename
|
31
31
|
def initialize content_type, desc, part
|
32
32
|
@content_type = content_type
|
33
33
|
@desc = desc
|
34
34
|
@part = part
|
35
35
|
@file = nil
|
36
|
+
desc =~ /filename="(.*?)"/ && @filename = $1
|
36
37
|
end
|
37
38
|
|
38
39
|
def view!
|
39
40
|
unless @file
|
40
41
|
@file = Tempfile.new "redwood.attachment"
|
41
|
-
@file.print
|
42
|
+
@file.print self
|
42
43
|
@file.close
|
43
44
|
end
|
44
45
|
|
45
46
|
## TODO: handle unknown mime-types
|
46
47
|
system "/usr/bin/run-mailcap --action=view #{@content_type}:#{@file.path}"
|
47
48
|
end
|
49
|
+
|
50
|
+
def to_s; @part.decode; end
|
48
51
|
end
|
49
52
|
|
50
53
|
class Text
|
@@ -73,25 +76,38 @@ class Message
|
|
73
76
|
BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
|
74
77
|
QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
|
75
78
|
SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)/
|
76
|
-
|
79
|
+
MAX_SIG_DISTANCE = 15 # lines from the end
|
77
80
|
DEFAULT_SUBJECT = "(missing subject)"
|
78
81
|
DEFAULT_SENDER = "(missing sender)"
|
79
82
|
|
80
83
|
attr_reader :id, :date, :from, :subj, :refs, :replytos, :to, :source,
|
81
84
|
:cc, :bcc, :labels, :list_address, :recipient_email, :replyto,
|
82
|
-
:source_info, :
|
85
|
+
:source_info, :status
|
83
86
|
|
84
87
|
bool_reader :dirty
|
85
88
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
+
## if index_entry is specified, will fill in values from that,
|
90
|
+
def initialize opts
|
91
|
+
if opts[:source]
|
92
|
+
@source = opts[:source]
|
93
|
+
@source_info = opts[:source_info] or raise ArgumentError, ":source but no :source_info"
|
94
|
+
@body = nil
|
95
|
+
else
|
96
|
+
@source = @source_info = nil
|
97
|
+
@body = opts[:body] or raise ArgumentError, "one of :body or :source must be specified"
|
98
|
+
end
|
99
|
+
@snippet = opts[:snippet] || ""
|
100
|
+
@labels = opts[:labels] || []
|
89
101
|
@dirty = false
|
90
|
-
@snippet = snippet
|
91
|
-
@labels = labels
|
92
102
|
|
93
|
-
header =
|
94
|
-
|
103
|
+
header =
|
104
|
+
if opts[:header]
|
105
|
+
opts[:header]
|
106
|
+
else
|
107
|
+
header = @source.load_header @source_info
|
108
|
+
header.each { |k, v| header[k.downcase] = v }
|
109
|
+
header
|
110
|
+
end
|
95
111
|
|
96
112
|
%w(message-id date).each do |f|
|
97
113
|
raise MessageFormatError, "no #{f} field in header #{header.inspect} (source #@source offset #@source_info)" unless header.include? f
|
@@ -99,7 +115,8 @@ class Message
|
|
99
115
|
end
|
100
116
|
|
101
117
|
begin
|
102
|
-
|
118
|
+
date = header["date"]
|
119
|
+
@date = (Time === date ? date : Time.parse(header["date"]))
|
103
120
|
rescue ArgumentError => e
|
104
121
|
raise MessageFormatError, "unparsable date #{header['date']}: #{e.message}"
|
105
122
|
end
|
@@ -125,14 +142,10 @@ class Message
|
|
125
142
|
end
|
126
143
|
|
127
144
|
@recipient_email = header["delivered-to"]
|
128
|
-
@
|
129
|
-
end
|
130
|
-
|
131
|
-
def snippet
|
132
|
-
to_chunks unless @snippet
|
133
|
-
@snippet
|
145
|
+
@status = header["status"]
|
134
146
|
end
|
135
147
|
|
148
|
+
def snippet; @snippet || to_chunks && @snippet; end
|
136
149
|
def is_list_message?; !@list_address.nil?; end
|
137
150
|
def is_draft?; DraftLoader === @source; end
|
138
151
|
def draft_filename
|
@@ -167,12 +180,19 @@ class Message
|
|
167
180
|
end
|
168
181
|
|
169
182
|
def to_chunks
|
170
|
-
|
171
|
-
|
183
|
+
if @body
|
184
|
+
[Text.new(@body.split("\n"))]
|
185
|
+
else
|
186
|
+
message_to_chunks @source.load_message(@source_info)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def raw_header
|
191
|
+
@source.raw_header @source_info
|
172
192
|
end
|
173
193
|
|
174
|
-
def
|
175
|
-
@source.
|
194
|
+
def raw_full_message
|
195
|
+
@source.raw_full_message @source_info
|
176
196
|
end
|
177
197
|
|
178
198
|
def content
|
@@ -210,7 +230,7 @@ private
|
|
210
230
|
m.body
|
211
231
|
body = m.decode or raise MessageFormatError, "no message body"
|
212
232
|
text_to_chunks body.gsub(/\t/, " ").gsub(/\r/, "").split("\n")
|
213
|
-
when
|
233
|
+
when /^multipart\//
|
214
234
|
nil
|
215
235
|
else
|
216
236
|
disp = m.header["Content-Disposition"] || ""
|
@@ -237,7 +257,7 @@ private
|
|
237
257
|
newstate = nil
|
238
258
|
if line =~ QUOTE_PATTERN || (line =~ QUOTE_START_PATTERN && (nextline =~ QUOTE_PATTERN || nextline =~ QUOTE_START_PATTERN))
|
239
259
|
newstate = :quote
|
240
|
-
elsif line =~ SIG_PATTERN && (lines.length - i) <
|
260
|
+
elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
|
241
261
|
newstate = :sig
|
242
262
|
elsif line =~ BLOCK_QUOTE_PATTERN
|
243
263
|
newstate = :block_quote
|
@@ -253,7 +273,7 @@ private
|
|
253
273
|
newstate = nil
|
254
274
|
if line =~ QUOTE_PATTERN || line =~ QUOTE_START_PATTERN || line =~ /^\s*$/
|
255
275
|
chunk_lines << line
|
256
|
-
elsif line =~ SIG_PATTERN && (lines.length - i) <
|
276
|
+
elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
|
257
277
|
newstate = :sig
|
258
278
|
else
|
259
279
|
newstate = :text
|
data/lib/sup/mode.rb
CHANGED
@@ -2,8 +2,8 @@ module Redwood
|
|
2
2
|
|
3
3
|
class BufferListMode < LineCursorMode
|
4
4
|
register_keymap do |k|
|
5
|
-
k.add :jump_to_buffer, "Jump to
|
6
|
-
k.add :reload, "Reload", "R"
|
5
|
+
k.add :jump_to_buffer, "Jump to selected buffer", :enter
|
6
|
+
k.add :reload, "Reload buffer list", "R"
|
7
7
|
end
|
8
8
|
|
9
9
|
def initialize
|
@@ -23,9 +23,9 @@ protected
|
|
23
23
|
|
24
24
|
def regen_text
|
25
25
|
@bufs = BufferManager.buffers.sort_by { |name, buf| name }
|
26
|
-
width = @bufs.map { |name, buf| name.length }.max
|
26
|
+
width = @bufs.map { |name, buf| buf.mode.name.length }.max
|
27
27
|
@text = @bufs.map do |name, buf|
|
28
|
-
sprintf "%#{width}s %s",
|
28
|
+
sprintf "%#{width}s %s", buf.mode.name, name
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -101,7 +101,7 @@ protected
|
|
101
101
|
end
|
102
102
|
|
103
103
|
def send_message
|
104
|
-
return false unless @edited || BufferManager.ask_yes_or_no("
|
104
|
+
return false unless @edited || BufferManager.ask_yes_or_no("Message unedited. Really send?")
|
105
105
|
|
106
106
|
raise "no message id!" unless header["Message-Id"]
|
107
107
|
date = Time.now
|
@@ -112,12 +112,11 @@ protected
|
|
112
112
|
AccountManager.default_account.email
|
113
113
|
end
|
114
114
|
|
115
|
-
|
116
|
-
raise "nil sendmail" unless sendmail
|
115
|
+
acct = AccountManager.account_for(from_email) || AccountManager.default_account
|
117
116
|
SentManager.write_sent_message(date, from_email) { |f| write_message f, true, date }
|
118
117
|
BufferManager.flash "sending..."
|
119
118
|
|
120
|
-
IO.popen(sendmail, "w") { |p| write_message p, true, date }
|
119
|
+
IO.popen(acct.sendmail, "w") { |p| write_message p, true, date }
|
121
120
|
|
122
121
|
BufferManager.kill_buffer buffer
|
123
122
|
BufferManager.flash "Message sent!"
|
@@ -128,6 +127,7 @@ protected
|
|
128
127
|
DraftManager.write_draft { |f| write_message f, false }
|
129
128
|
BufferManager.kill_buffer buffer
|
130
129
|
BufferManager.flash "Saved for later editing."
|
130
|
+
true
|
131
131
|
end
|
132
132
|
|
133
133
|
def sig_lines
|
data/lib/sup/modes/inbox-mode.rb
CHANGED