sup 0.5 → 0.6
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/CONTRIBUTORS +13 -0
- data/History.txt +10 -0
- data/Manifest.txt +1 -1
- data/Rakefile +3 -3
- data/ReleaseNotes +6 -0
- data/bin/sup +18 -58
- data/lib/sup/colormap.rb +94 -2
- data/lib/sup/crypto.rb +3 -1
- data/lib/sup/draft.rb +1 -1
- data/lib/sup/hook.rb +1 -1
- data/lib/sup/index.rb +16 -1
- data/lib/sup/keymap.rb +1 -7
- data/lib/sup/label.rb +3 -3
- data/lib/sup/maildir.rb +35 -17
- data/lib/sup/mbox.rb +18 -17
- data/lib/sup/message.rb +20 -3
- data/lib/sup/modes/compose-mode.rb +2 -0
- data/lib/sup/modes/edit-message-mode.rb +13 -12
- data/lib/sup/modes/file-browser-mode.rb +1 -1
- data/lib/sup/modes/forward-mode.rb +1 -1
- data/lib/sup/modes/inbox-mode.rb +18 -0
- data/lib/sup/modes/label-list-mode.rb +5 -0
- data/lib/sup/modes/reply-mode.rb +38 -2
- data/lib/sup/modes/scroll-mode.rb +11 -6
- data/lib/sup/modes/thread-index-mode.rb +11 -4
- data/lib/sup/modes/thread-view-mode.rb +9 -9
- data/lib/sup/util.rb +6 -2
- data/lib/sup.rb +30 -6
- data/test/test_mbox_parsing.rb +114 -0
- metadata +34 -5
- data/doc/TODO +0 -197
- data/test/test_maildir.rb +0 -25
data/lib/sup/message.rb
CHANGED
@@ -37,7 +37,7 @@ class Message
|
|
37
37
|
DEFAULT_SENDER = "(missing sender)"
|
38
38
|
|
39
39
|
attr_reader :id, :date, :from, :subj, :refs, :replytos, :to, :source,
|
40
|
-
:cc, :bcc, :labels, :list_address, :recipient_email, :replyto,
|
40
|
+
:cc, :bcc, :labels, :attachments, :list_address, :recipient_email, :replyto,
|
41
41
|
:source_info, :list_subscribe, :list_unsubscribe
|
42
42
|
|
43
43
|
bool_reader :dirty, :source_marked_read, :snippet_contains_encrypted_content
|
@@ -54,6 +54,7 @@ class Message
|
|
54
54
|
@dirty = false
|
55
55
|
@encrypted = false
|
56
56
|
@chunks = nil
|
57
|
+
@attachments = []
|
57
58
|
|
58
59
|
## we need to initialize this. see comments in parse_header as to
|
59
60
|
## why.
|
@@ -148,7 +149,18 @@ class Message
|
|
148
149
|
@source.fn_for_offset @source_info
|
149
150
|
end
|
150
151
|
|
151
|
-
|
152
|
+
## sanitize message ids by removing spaces and non-ascii characters.
|
153
|
+
## also, truncate to 255 characters. all these steps are necessary
|
154
|
+
## to make ferret happy. of course, we probably fuck up a couple
|
155
|
+
## valid message ids as well. as long as we're consistent, this
|
156
|
+
## should be fine, though.
|
157
|
+
##
|
158
|
+
## also, mostly the message ids that are changed by this belong to
|
159
|
+
## spam email.
|
160
|
+
##
|
161
|
+
## an alternative would be to SHA1 or MD5 all message ids on a regular basis.
|
162
|
+
## don't tempt me.
|
163
|
+
def sanitize_message_id mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end
|
152
164
|
|
153
165
|
def save index
|
154
166
|
return unless @dirty
|
@@ -405,6 +417,11 @@ private
|
|
405
417
|
|
406
418
|
## if there's a filename, we'll treat it as an attachment.
|
407
419
|
if filename
|
420
|
+
# add this to the attachments list if its not a generated html
|
421
|
+
# attachment (should we allow images with generated names?).
|
422
|
+
# Lowercase the filename because searches are easier that way
|
423
|
+
@attachments.push filename.downcase unless filename =~ /^sup-attachment-/
|
424
|
+
add_label :attachment unless filename =~ /^sup-attachment-/
|
408
425
|
[Chunk::Attachment.new(m.header.content_type, filename, m, sibling_types)]
|
409
426
|
|
410
427
|
## otherwise, it's body text
|
@@ -423,7 +440,7 @@ private
|
|
423
440
|
Iconv.iconv($encoding + "//IGNORE", charset, body + " ").join[0 .. -2]
|
424
441
|
rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence, MessageFormatError => e
|
425
442
|
Redwood::log "warning: error (#{e.class.name}) decoding message body from #{charset}: #{e.message}"
|
426
|
-
File.open("
|
443
|
+
File.open(File.join(BASE_DIR,"unable-to-decode.txt"), "w") { |f| f.write body }
|
427
444
|
body
|
428
445
|
end
|
429
446
|
end
|
@@ -9,6 +9,8 @@ class ComposeMode < EditMessageMode
|
|
9
9
|
header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
|
10
10
|
header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
|
11
11
|
header["Subject"] = opts[:subj] if opts[:subj]
|
12
|
+
header["References"] = opts[:refs].map { |r| "<#{r}>" }.join(" ") if opts[:refs]
|
13
|
+
header["In-Reply-To"] = opts[:replytos].map { |r| "<#{r}>" }.join(" ") if opts[:replytos]
|
12
14
|
|
13
15
|
super :header => header, :body => (opts[:body] || [])
|
14
16
|
end
|
@@ -23,7 +23,7 @@ Variables:
|
|
23
23
|
from_email: the email part of the From: line, or nil if empty
|
24
24
|
Return value:
|
25
25
|
A string (multi-line ok) containing the text of the signature, or nil to
|
26
|
-
use the default signature.
|
26
|
+
use the default signature, or :none for no signature.
|
27
27
|
EOS
|
28
28
|
|
29
29
|
HookManager.register "before-edit", <<EOS
|
@@ -122,7 +122,7 @@ EOS
|
|
122
122
|
@file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
|
123
123
|
@file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
|
124
124
|
@file.puts
|
125
|
-
@file.puts @body
|
125
|
+
@file.puts @body.join("\n")
|
126
126
|
@file.close
|
127
127
|
|
128
128
|
editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
|
@@ -213,7 +213,7 @@ protected
|
|
213
213
|
def parse_file fn
|
214
214
|
File.open(fn) do |f|
|
215
215
|
header = MBox::read_header f
|
216
|
-
body = f.readlines
|
216
|
+
body = f.readlines.map { |l| l.chomp }
|
217
217
|
|
218
218
|
header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
|
219
219
|
header.each { |k, v| header[k] = parse_header k, v }
|
@@ -304,9 +304,10 @@ protected
|
|
304
304
|
def build_message date
|
305
305
|
m = RMail::Message.new
|
306
306
|
m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
|
307
|
-
m.body = @body.join
|
308
|
-
m.body = m.body
|
307
|
+
m.body = @body.join("\n")
|
309
308
|
m.body += sig_lines.join("\n") unless $config[:edit_signature]
|
309
|
+
## body must end in a newline or GPG signatures will be WRONG!
|
310
|
+
m.body += "\n" unless m.body =~ /\n\Z/
|
310
311
|
|
311
312
|
## there are attachments, so wrap body in an attachment of its own
|
312
313
|
unless @attachments.empty?
|
@@ -321,7 +322,7 @@ protected
|
|
321
322
|
## do whatever crypto transformation is necessary
|
322
323
|
if @crypto_selector && @crypto_selector.val != :none
|
323
324
|
from_email = PersonManager.person_for(@header["From"]).email
|
324
|
-
to_email =
|
325
|
+
to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| PersonManager.person_for(p).email }
|
325
326
|
|
326
327
|
m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
|
327
328
|
end
|
@@ -364,7 +365,7 @@ EOS
|
|
364
365
|
end
|
365
366
|
|
366
367
|
f.puts
|
367
|
-
f.puts sanitize_body(@body.join)
|
368
|
+
f.puts sanitize_body(@body.join("\n"))
|
368
369
|
f.puts sig_lines if full unless $config[:edit_signature]
|
369
370
|
end
|
370
371
|
|
@@ -377,12 +378,11 @@ protected
|
|
377
378
|
if text
|
378
379
|
@header[field] = parse_header field, text
|
379
380
|
update
|
380
|
-
field
|
381
381
|
end
|
382
382
|
else
|
383
|
-
default =
|
384
|
-
case field
|
383
|
+
default = case field
|
385
384
|
when *MULTI_HEADERS
|
385
|
+
@header[field] ||= []
|
386
386
|
@header[field].join(", ")
|
387
387
|
else
|
388
388
|
@header[field]
|
@@ -393,7 +393,6 @@ protected
|
|
393
393
|
text = contacts.map { |s| s.longname }.join(", ")
|
394
394
|
@header[field] = parse_header field, text
|
395
395
|
update
|
396
|
-
field
|
397
396
|
end
|
398
397
|
end
|
399
398
|
end
|
@@ -409,7 +408,7 @@ private
|
|
409
408
|
end
|
410
409
|
|
411
410
|
def top_posting?
|
412
|
-
@body.join =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
|
411
|
+
@body.join("\n") =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
|
413
412
|
end
|
414
413
|
|
415
414
|
def sig_lines
|
@@ -418,6 +417,8 @@ private
|
|
418
417
|
|
419
418
|
## first run the hook
|
420
419
|
hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email
|
420
|
+
|
421
|
+
return [] if hook_sig == :none
|
421
422
|
return ["", "-- "] + hook_sig.split("\n") if hook_sig
|
422
423
|
|
423
424
|
## no hook, do default signature generation based on config.yaml
|
@@ -42,7 +42,7 @@ class ForwardMode < EditMessageMode
|
|
42
42
|
end
|
43
43
|
|
44
44
|
attachments.each do |c|
|
45
|
-
mime_type = MIME::Types[c.content_type].first || MIME::Types["application/octet-stream"]
|
45
|
+
mime_type = MIME::Types[c.content_type].first || MIME::Types["application/octet-stream"].first
|
46
46
|
attachment_hash[c.filename] = RMail::Message.make_attachment c.raw_content, mime_type.content_type, mime_type.encoding, c.filename
|
47
47
|
end
|
48
48
|
|
data/lib/sup/modes/inbox-mode.rb
CHANGED
@@ -6,6 +6,7 @@ class InboxMode < ThreadIndexMode
|
|
6
6
|
register_keymap do |k|
|
7
7
|
## overwrite toggle_archived with archive
|
8
8
|
k.add :archive, "Archive thread (remove from inbox)", 'a'
|
9
|
+
k.add :read_and_archive, "Archive thread (remove from inbox) and mark read", 'A'
|
9
10
|
end
|
10
11
|
|
11
12
|
def initialize
|
@@ -38,6 +39,23 @@ class InboxMode < ThreadIndexMode
|
|
38
39
|
regen_text
|
39
40
|
end
|
40
41
|
|
42
|
+
def read_and_archive
|
43
|
+
return unless cursor_thread
|
44
|
+
cursor_thread.remove_label :unread
|
45
|
+
cursor_thread.remove_label :inbox
|
46
|
+
hide_thread cursor_thread
|
47
|
+
regen_text
|
48
|
+
end
|
49
|
+
|
50
|
+
def multi_read_and_archive threads
|
51
|
+
threads.each do |t|
|
52
|
+
t.remove_label :unread
|
53
|
+
t.remove_label :inbox
|
54
|
+
hide_thread t
|
55
|
+
end
|
56
|
+
regen_text
|
57
|
+
end
|
58
|
+
|
41
59
|
def handle_unarchived_update sender, m
|
42
60
|
add_or_unhide m
|
43
61
|
end
|
data/lib/sup/modes/reply-mode.rb
CHANGED
@@ -19,6 +19,27 @@ Return value:
|
|
19
19
|
A string containing the text of the quote line (can be multi-line)
|
20
20
|
EOS
|
21
21
|
|
22
|
+
HookManager.register "reply-from", <<EOS
|
23
|
+
Selects a default address for the From: header of a new reply.
|
24
|
+
Variables:
|
25
|
+
message: a message object representing the message being replied to
|
26
|
+
(useful values include message.recipient_email, message.to, and message.cc)
|
27
|
+
Return value:
|
28
|
+
A Person to be used as the default for the From: header, or nil to use the
|
29
|
+
default behavior.
|
30
|
+
EOS
|
31
|
+
|
32
|
+
HookManager.register "reply-to", <<EOS
|
33
|
+
Set the default reply-to mode.
|
34
|
+
Variables:
|
35
|
+
modes: array of valid modes to choose from, which will be a subset of
|
36
|
+
[:#{REPLY_TYPES * ', :'}]
|
37
|
+
The default behavior is equivalent to
|
38
|
+
([:list, :sender, :recipent] & modes)[0]
|
39
|
+
Return value:
|
40
|
+
The reply mode you desire, or nil to use the default behavior.
|
41
|
+
EOS
|
42
|
+
|
22
43
|
def initialize message
|
23
44
|
@m = message
|
24
45
|
|
@@ -29,8 +50,19 @@ EOS
|
|
29
50
|
|
30
51
|
## first, determine the address at which we received this email. this will
|
31
52
|
## become our From: address in the reply.
|
53
|
+
hook_reply_from = HookManager.run "reply-from", :message => @m
|
54
|
+
|
55
|
+
## sanity check that selection is a Person (or we'll fail below)
|
56
|
+
## don't check that it's an Account, though; assume they know what they're doing.
|
57
|
+
if hook_reply_from && !(hook_reply_from.is_a? Person)
|
58
|
+
Redwood::log "reply-from returned non-Person, using default from."
|
59
|
+
hook_reply_from = nil
|
60
|
+
end
|
61
|
+
|
32
62
|
from =
|
33
|
-
if
|
63
|
+
if hook_reply_from
|
64
|
+
hook_reply_from
|
65
|
+
elsif @m.recipient_email && AccountManager.is_account_email?(@m.recipient_email)
|
34
66
|
PersonManager.person_for(@m.recipient_email)
|
35
67
|
elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
|
36
68
|
b
|
@@ -92,8 +124,12 @@ EOS
|
|
92
124
|
types = REPLY_TYPES.select { |t| @headers.member?(t) }
|
93
125
|
@type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
|
94
126
|
|
127
|
+
hook_reply = HookManager.run "reply-to", :modes => types
|
128
|
+
|
95
129
|
@type_selector.set_to(
|
96
|
-
if
|
130
|
+
if types.include? hook_reply
|
131
|
+
hook_reply
|
132
|
+
elsif @m.is_list_message?
|
97
133
|
:list
|
98
134
|
elsif @headers.member? :sender
|
99
135
|
:sender
|
@@ -15,12 +15,14 @@ class ScrollMode < Mode
|
|
15
15
|
COL_JUMP = 2
|
16
16
|
|
17
17
|
register_keymap do |k|
|
18
|
-
k.add :line_down, "Down one line", :down, 'j', 'J'
|
19
|
-
k.add :line_up, "Up one line", :up, 'k', 'K'
|
18
|
+
k.add :line_down, "Down one line", :down, 'j', 'J', "\C-e"
|
19
|
+
k.add :line_up, "Up one line", :up, 'k', 'K', "\C-y"
|
20
20
|
k.add :col_left, "Left one column", :left, 'h'
|
21
21
|
k.add :col_right, "Right one column", :right, 'l'
|
22
|
-
k.add :page_down, "Down one page", :page_down, ' '
|
23
|
-
k.add :page_up, "Up one page", :page_up, 'p', :backspace
|
22
|
+
k.add :page_down, "Down one page", :page_down, ' ', "\C-f"
|
23
|
+
k.add :page_up, "Up one page", :page_up, 'p', :backspace, "\C-b"
|
24
|
+
k.add :half_page_down, "Down one half page", "\C-d"
|
25
|
+
k.add :half_page_up, "Up one half page", "\C-u"
|
24
26
|
k.add :jump_to_start, "Jump to top", :home, '^', '1'
|
25
27
|
k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
|
26
28
|
k.add :jump_to_left, "Jump to the left", '['
|
@@ -85,15 +87,16 @@ class ScrollMode < Mode
|
|
85
87
|
continue_search_in_buffer
|
86
88
|
end
|
87
89
|
|
88
|
-
## subclasses can override these
|
90
|
+
## subclasses can override these three!
|
89
91
|
def search_goto_pos line, leftcol, rightcol
|
90
|
-
|
92
|
+
search_goto_line line
|
91
93
|
|
92
94
|
if rightcol > self.rightcol # if it's occluded...
|
93
95
|
jump_to_col [rightcol - buffer.content_width + 1, 0].max # move right
|
94
96
|
end
|
95
97
|
end
|
96
98
|
def search_start_line; @topline end
|
99
|
+
def search_goto_line line; jump_to_line line end
|
97
100
|
|
98
101
|
def col_left
|
99
102
|
return unless @leftcol > 0
|
@@ -130,6 +133,8 @@ class ScrollMode < Mode
|
|
130
133
|
def line_up; jump_to_line @topline - 1; end
|
131
134
|
def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end
|
132
135
|
def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end
|
136
|
+
def half_page_down; jump_to_line @topline + buffer.content_height / 2; end
|
137
|
+
def half_page_up; jump_to_line @topline - buffer.content_height / 2; end
|
133
138
|
def jump_to_start; jump_to_line 0; end
|
134
139
|
def jump_to_end; jump_to_line lines - buffer.content_height; end
|
135
140
|
|
@@ -14,6 +14,12 @@ Variables:
|
|
14
14
|
thread: The message thread to be formatted.
|
15
15
|
EOS
|
16
16
|
|
17
|
+
HookManager.register "mark-as-spam", <<EOS
|
18
|
+
This hook is run when a thread is marked as spam
|
19
|
+
Variables:
|
20
|
+
thread: The message thread being marked as spam.
|
21
|
+
EOS
|
22
|
+
|
17
23
|
register_keymap do |k|
|
18
24
|
k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
|
19
25
|
k.add_multi "Load all threads (! to confirm) :", '!' do |kk|
|
@@ -333,6 +339,7 @@ EOS
|
|
333
339
|
def toggle_spam
|
334
340
|
t = cursor_thread or return
|
335
341
|
multi_toggle_spam [t]
|
342
|
+
HookManager.run("mark-as-spam", :thread => t)
|
336
343
|
end
|
337
344
|
|
338
345
|
## both spam and deleted have the curious characteristic that you
|
@@ -440,9 +447,8 @@ EOS
|
|
440
447
|
end
|
441
448
|
|
442
449
|
def multi_edit_labels threads
|
443
|
-
|
444
|
-
return unless
|
445
|
-
user_labels = answer.split(/\s+/).map { |l| l.intern }
|
450
|
+
user_labels = BufferManager.ask_for_labels :add_labels, "Add labels: ", [], @hidden_labels
|
451
|
+
return unless user_labels
|
446
452
|
|
447
453
|
hl = user_labels.select { |l| @hidden_labels.member? l }
|
448
454
|
if hl.empty?
|
@@ -713,7 +719,8 @@ protected
|
|
713
719
|
from +
|
714
720
|
[
|
715
721
|
[subj_color, size_widget_text],
|
716
|
-
[:to_me_color,
|
722
|
+
[:to_me_color, t.labels.member?(:attachment) ? "@" : " "],
|
723
|
+
[:to_me_color, dp ? ">" : (p ? '+' : " ")],
|
717
724
|
[subj_color, t.subj + (t.subj.empty? ? "" : " ")],
|
718
725
|
] +
|
719
726
|
(t.labels - @hidden_labels).map { |label| [:label_color, "+#{label} "] } +
|
@@ -188,7 +188,7 @@ EOS
|
|
188
188
|
def compose
|
189
189
|
p = @person_lines[curpos]
|
190
190
|
if p
|
191
|
-
ComposeMode.spawn_nicely :
|
191
|
+
ComposeMode.spawn_nicely :to_default => p
|
192
192
|
else
|
193
193
|
ComposeMode.spawn_nicely
|
194
194
|
end
|
@@ -250,7 +250,7 @@ EOS
|
|
250
250
|
|
251
251
|
def edit_as_new
|
252
252
|
m = @message_lines[curpos] or return
|
253
|
-
mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc)
|
253
|
+
mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc, :refs => m.refs, :replytos => m.replytos)
|
254
254
|
BufferManager.spawn "edit as new", mode
|
255
255
|
mode.edit_message
|
256
256
|
end
|
@@ -562,29 +562,29 @@ private
|
|
562
562
|
|
563
563
|
open_widget = [color, (state == :closed ? "+ " : "- ")]
|
564
564
|
new_widget = [color, (m.has_label?(:unread) ? "N" : " ")]
|
565
|
-
starred_widget =
|
566
|
-
|
567
|
-
[star_color, "* "]
|
565
|
+
starred_widget = if m.has_label?(:starred)
|
566
|
+
[star_color, "*"]
|
568
567
|
else
|
569
|
-
[color, "
|
568
|
+
[color, " "]
|
570
569
|
end
|
570
|
+
attach_widget = [color, (m.has_label?(:attachment) ? "@" : " ")]
|
571
571
|
|
572
572
|
case state
|
573
573
|
when :open
|
574
574
|
@person_lines[start] = m.from
|
575
|
-
[[prefix_widget, open_widget, new_widget, starred_widget,
|
575
|
+
[[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
576
576
|
[color,
|
577
577
|
"#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
|
578
578
|
|
579
579
|
when :closed
|
580
580
|
@person_lines[start] = m.from
|
581
|
-
[[prefix_widget, open_widget, new_widget, starred_widget,
|
581
|
+
[[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
582
582
|
[color,
|
583
583
|
"#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]]
|
584
584
|
|
585
585
|
when :detailed
|
586
586
|
@person_lines[start] = m.from
|
587
|
-
from_line = [[prefix_widget, open_widget, new_widget, starred_widget,
|
587
|
+
from_line = [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
588
588
|
[color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
|
589
589
|
|
590
590
|
addressee_lines = []
|
data/lib/sup/util.rb
CHANGED
@@ -108,7 +108,9 @@ class Module
|
|
108
108
|
def defer_all_other_method_calls_to obj
|
109
109
|
class_eval %{
|
110
110
|
def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
|
111
|
-
def respond_to?
|
111
|
+
def respond_to?(m, include_private = false)
|
112
|
+
@#{obj}.respond_to?(m, include_private)
|
113
|
+
end
|
112
114
|
}
|
113
115
|
end
|
114
116
|
end
|
@@ -527,7 +529,9 @@ class Recoverable
|
|
527
529
|
def to_yaml x; __pass :to_yaml, x; end
|
528
530
|
def is_a? c; @o.is_a? c; end
|
529
531
|
|
530
|
-
def respond_to?
|
532
|
+
def respond_to?(m, include_private=false)
|
533
|
+
@o.respond_to?(m, include_private)
|
534
|
+
end
|
531
535
|
|
532
536
|
def __pass m, *a, &b
|
533
537
|
begin
|
data/lib/sup.rb
CHANGED
@@ -6,6 +6,19 @@ require 'fileutils'
|
|
6
6
|
require 'gettext'
|
7
7
|
require 'curses'
|
8
8
|
|
9
|
+
## the following magic enables wide characters when used with a ruby
|
10
|
+
## ncurses.so that's been compiled against libncursesw. (note the w.) why
|
11
|
+
## this works, i have no idea. much like pretty much every aspect of
|
12
|
+
## dealing with curses. cargo cult programming at its best.
|
13
|
+
|
14
|
+
require 'dl/import'
|
15
|
+
module LibC
|
16
|
+
extend DL::Importable
|
17
|
+
dlload Config::CONFIG['arch'] =~ /darwin/ ? "libc.dylib" : "libc.so.6"
|
18
|
+
extern "void setlocale(int, const char *)"
|
19
|
+
end
|
20
|
+
LibC.setlocale(6, "") # LC_ALL == 6
|
21
|
+
|
9
22
|
class Object
|
10
23
|
## this is for debugging purposes because i keep calling #id on the
|
11
24
|
## wrong object and i want it to throw an exception
|
@@ -33,10 +46,11 @@ class Module
|
|
33
46
|
end
|
34
47
|
|
35
48
|
module Redwood
|
36
|
-
VERSION = "0.
|
49
|
+
VERSION = "0.6"
|
37
50
|
|
38
51
|
BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
|
39
52
|
CONFIG_FN = File.join(BASE_DIR, "config.yaml")
|
53
|
+
COLOR_FN = File.join(BASE_DIR, "colors.yaml")
|
40
54
|
SOURCE_FN = File.join(BASE_DIR, "sources.yaml")
|
41
55
|
LABEL_FN = File.join(BASE_DIR, "labels.txt")
|
42
56
|
PERSON_FN = File.join(BASE_DIR, "people.txt")
|
@@ -50,7 +64,18 @@ module Redwood
|
|
50
64
|
YAML_DOMAIN = "masanjin.net"
|
51
65
|
YAML_DATE = "2006-10-01"
|
52
66
|
|
53
|
-
## record exceptions thrown in threads nicely
|
67
|
+
## record exceptions thrown in threads nicely
|
68
|
+
@exceptions = []
|
69
|
+
@exception_mutex = Mutex.new
|
70
|
+
|
71
|
+
attr_reader :exceptions
|
72
|
+
def record_exception e, name
|
73
|
+
@exception_mutex.synchronize do
|
74
|
+
@exceptions ||= []
|
75
|
+
@exceptions << [e, name]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
54
79
|
def reporting_thread name
|
55
80
|
if $opts[:no_threads]
|
56
81
|
yield
|
@@ -59,14 +84,13 @@ module Redwood
|
|
59
84
|
begin
|
60
85
|
yield
|
61
86
|
rescue Exception => e
|
62
|
-
|
63
|
-
$exceptions << [e, name]
|
64
|
-
raise
|
87
|
+
record_exception e, name
|
65
88
|
end
|
66
89
|
end
|
67
90
|
end
|
68
91
|
end
|
69
|
-
|
92
|
+
|
93
|
+
module_function :reporting_thread, :record_exception, :exceptions
|
70
94
|
|
71
95
|
## one-stop shop for yamliciousness
|
72
96
|
def save_yaml_obj object, fn, safe=false
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'sup'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
include Redwood
|
8
|
+
|
9
|
+
class TestMessage < Test::Unit::TestCase
|
10
|
+
def setup
|
11
|
+
end
|
12
|
+
|
13
|
+
def teardown
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_normal_headers
|
17
|
+
h = MBox.read_header StringIO.new(<<EOS)
|
18
|
+
From: Bob <bob@bob.com>
|
19
|
+
To: Sally <sally@sally.com>
|
20
|
+
EOS
|
21
|
+
|
22
|
+
assert_equal "Bob <bob@bob.com>", h["From"]
|
23
|
+
assert_equal "Sally <sally@sally.com>", h["To"]
|
24
|
+
assert_nil h["Message-Id"]
|
25
|
+
end
|
26
|
+
|
27
|
+
## this is shitty behavior in retrospect, but it's built in now.
|
28
|
+
def test_message_id_stripping
|
29
|
+
h = MBox.read_header StringIO.new("Message-Id: <one@bob.com>\n")
|
30
|
+
assert_equal "one@bob.com", h["Message-Id"]
|
31
|
+
|
32
|
+
h = MBox.read_header StringIO.new("Message-Id: one@bob.com\n")
|
33
|
+
assert_equal "one@bob.com", h["Message-Id"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_multiline
|
37
|
+
h = MBox.read_header StringIO.new(<<EOS)
|
38
|
+
From: Bob <bob@bob.com>
|
39
|
+
Subject: one two three
|
40
|
+
four five six
|
41
|
+
To: Sally <sally@sally.com>
|
42
|
+
References: seven
|
43
|
+
eight
|
44
|
+
Seven: Eight
|
45
|
+
EOS
|
46
|
+
|
47
|
+
assert_equal "one two three four five six", h["Subject"]
|
48
|
+
assert_equal "Sally <sally@sally.com>", h["To"]
|
49
|
+
assert_equal "seven eight", h["References"]
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_ignore_spacing
|
53
|
+
variants = [
|
54
|
+
"Subject:one two three end\n",
|
55
|
+
"Subject: one two three end\n",
|
56
|
+
"Subject: one two three end \n",
|
57
|
+
]
|
58
|
+
variants.each do |s|
|
59
|
+
h = MBox.read_header StringIO.new(s)
|
60
|
+
assert_equal "one two three end", h["Subject"]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_message_id_ignore_spacing
|
65
|
+
variants = [
|
66
|
+
"Message-Id: <one@bob.com> \n",
|
67
|
+
"Message-Id: one@bob.com \n",
|
68
|
+
"Message-Id:<one@bob.com> \n",
|
69
|
+
"Message-Id:one@bob.com \n",
|
70
|
+
]
|
71
|
+
variants.each do |s|
|
72
|
+
h = MBox.read_header StringIO.new(s)
|
73
|
+
assert_equal "one@bob.com", h["Message-Id"]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_ignore_empty_lines
|
78
|
+
variants = [
|
79
|
+
"",
|
80
|
+
"Message-Id: \n",
|
81
|
+
"Message-Id:\n",
|
82
|
+
]
|
83
|
+
variants.each do |s|
|
84
|
+
h = MBox.read_header StringIO.new(s)
|
85
|
+
assert_nil h["Message-Id"]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_detect_end_of_headers
|
90
|
+
h = MBox.read_header StringIO.new(<<EOS)
|
91
|
+
From: Bob <bob@bob.com>
|
92
|
+
|
93
|
+
To: a dear friend
|
94
|
+
EOS
|
95
|
+
assert_equal "Bob <bob@bob.com>", h["From"]
|
96
|
+
assert_nil h["To"]
|
97
|
+
|
98
|
+
h = MBox.read_header StringIO.new(<<EOS)
|
99
|
+
From: Bob <bob@bob.com>
|
100
|
+
\r
|
101
|
+
To: a dear friend
|
102
|
+
EOS
|
103
|
+
assert_equal "Bob <bob@bob.com>", h["From"]
|
104
|
+
assert_nil h["To"]
|
105
|
+
|
106
|
+
h = MBox.read_header StringIO.new(<<EOS)
|
107
|
+
From: Bob <bob@bob.com>
|
108
|
+
\r\n\r
|
109
|
+
To: a dear friend
|
110
|
+
EOS
|
111
|
+
assert_equal "Bob <bob@bob.com>", h["From"]
|
112
|
+
assert_nil h["To"]
|
113
|
+
end
|
114
|
+
end
|