commit 1af11cb1d6f6686bc0b4cd1b416b411c2c9dafb4
parent 4bf989f99f4edf59b26b0a72ca5345895acb1018
Author: William Morgan <wmorgan-sup@masanjin.net>
Date: Tue, 22 Jan 2008 12:21:46 -0800
Merge branch 'quote-detection'
Diffstat:
8 files changed, 110 insertions(+), 72 deletions(-)
diff --git a/lib/sup/imap.rb b/lib/sup/imap.rb
@@ -5,39 +5,39 @@ require 'time'
require 'rmail'
require 'cgi'
-## fucking imap fucking sucks. what the FUCK kind of committee of
-## dunces designed this shit.
+## fucking imap fucking sucks. what the FUCK kind of committee of dunces
+## designed this shit.
##
## imap talks about 'unique ids' for messages, to be used for
-## cross-session identification. great---just what sup needs! except
-## it turns out the uids can be invalidated every time the
-## 'uidvalidity' value changes on the server, and 'uidvalidity' can
-## change without restriction. it can change any time you log in. it
-## can change EVERY time you log in. of course the imap spec "strongly
-## recommends" that it never change, but there's nothing to stop
-## people from just setting it to the current timestamp, and in fact
-## that's exactly what the one imap server i have at my disposal
-## does. thus the so-called uids are absolutely useless and imap
-## provides no cross-session way of uniquely identifying a
-## message. but thanks for the "strong recommendation", guys!
+## cross-session identification. great---just what sup needs! except it
+## turns out the uids can be invalidated every time the 'uidvalidity'
+## value changes on the server, and 'uidvalidity' can change without
+## restriction. it can change any time you log in. it can change EVERY
+## time you log in. of course the imap spec "strongly recommends" that it
+## never change, but there's nothing to stop people from just setting it
+## to the current timestamp, and in fact that's exactly what the one imap
+## server i have at my disposal does. thus the so-called uids are
+## absolutely useless and imap provides no cross-session way of uniquely
+## identifying a message. but thanks for the "strong recommendation",
+## guys!
##
## so right now i'm using the 'internal date' and the size of each
## message to uniquely identify it, and i scan over the entire mailbox
## each time i open it to map those things to message ids. that can be
-## slow for large mailboxes, and we'll just have to hope that there
-## are no collisions. ho ho! a perfectly reasonable solution!
+## slow for large mailboxes, and we'll just have to hope that there are
+## no collisions. ho ho! a perfectly reasonable solution!
##
## and here's another thing. check out RFC2060 2.2.2 paragraph 5:
##
-## A client MUST be prepared to accept any server response at all times.
-## This includes server data that was not requested.
+## A client MUST be prepared to accept any server response at all
+## times. This includes server data that was not requested.
##
-## yeah. that totally makes a lot of sense. and once again, the idiocy
-## of the spec actually happens in practice. you'll request flags for
-## one message, and get it interspersed with a random bunch of flags
-## for some other messages, including a different set of flags for the
-## same message! totally ok by the imap spec. totally retarded by any
-## other metric.
+## yeah. that totally makes a lot of sense. and once again, the idiocy of
+## the spec actually happens in practice. you'll request flags for one
+## message, and get it interspersed with a random bunch of flags for some
+## other messages, including a different set of flags for the same
+## message! totally ok by the imap spec. totally retarded by any other
+## metric.
##
## fuck you, imap committee. you managed to design something nearly as
## shitty as mbox but goddamn THIRTY YEARS LATER.
@@ -205,11 +205,11 @@ private
def unsafe_connect
say "Connecting to IMAP server #{host}:#{port}..."
- ## apparently imap.rb does a lot of threaded stuff internally and
- ## if an exception occurs, it will catch it and re-raise it on the
- ## calling thread. but i can't seem to catch that exception, so
- ## i've resorted to initializing it in its own thread. surely
- ## there's a better way.
+ ## apparently imap.rb does a lot of threaded stuff internally and if
+ ## an exception occurs, it will catch it and re-raise it on the
+ ## calling thread. but i can't seem to catch that exception, so i've
+ ## resorted to initializing it in its own thread. surely there's a
+ ## better way.
exception = nil
::Thread.new do
begin
@@ -217,9 +217,9 @@ private
@imap = Net::IMAP.new host, port, ssl?
say "Logging in..."
- ## although RFC1730 claims that "If an AUTHENTICATE command
- ## fails with a NO response, the client may try another", in
- ## practice it seems like they can also send a BAD response.
+ ## although RFC1730 claims that "If an AUTHENTICATE command fails
+ ## with a NO response, the client may try another", in practice
+ ## it seems like they can also send a BAD response.
begin
raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=CRAM-MD5"
@imap.authenticate 'CRAM-MD5', @username, @password
@@ -271,19 +271,20 @@ private
result = fetch(imap_id, (fields + ['RFC822.SIZE', 'INTERNALDATE']).uniq).first
got_id = make_id result
- ## I've turned off the following sanity check because Microsoft Exchange fails it.
- ## Exchange actually reports two different INTERNALDATEs for the exact same message
- ## when queried at different points in time.
+ ## I've turned off the following sanity check because Microsoft
+ ## Exchange fails it. Exchange actually reports two different
+ ## INTERNALDATEs for the exact same message when queried at different
+ ## points in time.
##
- ##
- ## RFC2060 defines the semantics of INTERNALDATE for messages that arrive
- ## via SMTP for via various IMAP commands, but states that "All other
- ## cases are implementation defined.". Great, thanks guys, yet another
- ## useless field.
+ ## RFC2060 defines the semantics of INTERNALDATE for messages that
+ ## arrive via SMTP for via various IMAP commands, but states that
+ ## "All other cases are implementation defined.". Great, thanks guys,
+ ## yet another useless field.
##
- ## Of course no OTHER imap server I've encountered returns DIFFERENT values for
- ## the SAME message. But it's Microsoft; what do you expect? If their programmers
- ## were any good they'd be working at Google.
+ ## Of course no OTHER imap server I've encountered returns DIFFERENT
+ ## values for the SAME message. But it's Microsoft; what do you
+ ## expect? If their programmers were any good they'd be working at
+ ## Google.
# raise OutOfSyncSourceError, "IMAP message mismatch: requested #{id}, got #{got_id}." unless got_id == id
diff --git a/lib/sup/message-chunks.rb b/lib/sup/message-chunks.rb
@@ -82,6 +82,7 @@ EOS
:sibling_types => sibling_types
end
+ @lines = nil
if text
@lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
@quotable = true
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -29,7 +29,7 @@ class Message
QUOTE_PATTERN = /^\s{0,4}[>|\}]/
BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
- QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
+ QUOTE_START_PATTERN = /\w.*:$/
SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)|(^\s*--~--~-)|(^\s*--\+\+\*\*==)/
MAX_SIG_DISTANCE = 15 # lines from the end
@@ -60,25 +60,28 @@ class Message
def parse_header header
header.each { |k, v| header[k.downcase] = v }
-
+
+ fakeid = nil
+ fakename = nil
+
@id =
if header["message-id"]
sanitize_message_id header["message-id"]
else
- returning("sup-faked-" + Digest::MD5.hexdigest(raw_header)) do |id|
- Redwood::log "faking message-id for message from #@from: #{id}"
- end
+ fakeid = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
end
@from =
if header["from"]
PersonManager.person_for header["from"]
else
- name = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
- Redwood::log "faking from for message #@id: #{name}"
- PersonManager.person_for name
+ fakename = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
+ PersonManager.person_for fakename
end
+ Redwood::log "faking message-id for message from #@from: #{id}" if fakeid
+ Redwood::log "faking from for message #@id: #{fakename}" if fakename
+
date = header["date"]
@date =
case date
@@ -417,7 +420,7 @@ private
when :text
newstate = nil
- if line =~ QUOTE_PATTERN || (line =~ QUOTE_START_PATTERN && (nextline =~ QUOTE_PATTERN || nextline =~ QUOTE_START_PATTERN))
+ if line =~ QUOTE_PATTERN || (line =~ QUOTE_START_PATTERN && nextline =~ QUOTE_PATTERN)
newstate = :quote
elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
newstate = :sig
@@ -436,7 +439,7 @@ private
when :quote
newstate = nil
- if line =~ QUOTE_PATTERN || line =~ QUOTE_START_PATTERN #|| line =~ /^\s*$/
+ if line =~ QUOTE_PATTERN || (line =~ /^\s*$/ && nextline =~ QUOTE_PATTERN)
chunk_lines << line
elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
newstate = :sig
diff --git a/lib/sup/modes/reply-mode.rb b/lib/sup/modes/reply-mode.rb
@@ -10,6 +10,14 @@ class ReplyMode < EditMessageMode
:user => "Customized"
}
+ HookManager.register "quoteline", <<EOS
+Generates a quote line "On 1/4/2007, Joe Bloggs wrote:".
+Variables:
+ message: A message object representing the message being replied to
+Return value:
+ A string containing the text of the quote line (can be multi-line)
+EOS
+
def initialize message
@m = message
@@ -115,11 +123,16 @@ protected
end
def reply_body_lines m
- lines = ["Excerpts from #{@m.from.name}'s message of #{@m.date}:"] + m.quotable_body_lines.map { |l| "> #{l}" }
+ quoteline = HookManager.run("quoteline", :message => m) || default_quoteline(m)
+ lines = quoteline.split("\n") + m.quotable_body_lines.map { |l| "> #{l}" }
lines.pop while lines.last =~ /^\s*$/
lines
end
+ def default_quoteline m
+ "Excerpts from #{@m.from.name}'s message of #{@m.date}:"
+ end
+
def handle_new_text new_header, new_body
old_header = @headers[@type_selector.val]
if new_header.size != old_header.size || old_header.any? { |k, v| new_header[k] != v }
diff --git a/lib/sup/modes/scroll-mode.rb b/lib/sup/modes/scroll-mode.rb
@@ -12,7 +12,7 @@ class ScrollMode < Mode
attr_reader :status, :topline, :botline, :leftcol
- COL_JUMP = 2
+ COL_JUMP = 4
register_keymap do |k|
k.add :line_down, "Down one line", :down, 'j', 'J'
@@ -101,6 +101,7 @@ class ScrollMode < Mode
end
def jump_to_col col
+ col = col - (col % COL_JUMP)
buffer.mark_dirty unless @leftcol == col
@leftcol = col
end
diff --git a/lib/sup/modes/text-mode.rb b/lib/sup/modes/text-mode.rb
@@ -7,15 +7,16 @@ class TextMode < ScrollMode
k.add :pipe, "Pipe to process", '|'
end
- def initialize text=""
+ def initialize text="", filename=nil
@text = text
+ @filename = filename
update_lines
buffer.mark_dirty if buffer
super()
end
def save_to_disk
- fn = BufferManager.ask_for_filename :filename, "Save to file: "
+ fn = BufferManager.ask_for_filename :filename, "Save to file: ", @filename
save_to_file(fn) { |f| f.puts text } if fn
end
diff --git a/lib/sup/modes/thread-index-mode.rb b/lib/sup/modes/thread-index-mode.rb
@@ -91,7 +91,7 @@ EOS
mode = ThreadViewMode.new t, @hidden_labels, self
BufferManager.spawn t.subj, mode
BufferManager.draw_screen
- mode.jump_to_first_open
+ mode.jump_to_first_open true
BufferManager.draw_screen # lame TODO: make this unnecessary
## the first draw_screen is needed before topline and botline
## are set, and the second to show the cursor having moved
diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb
@@ -34,6 +34,7 @@ EOS
k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
k.add :jump_to_next_open, "Jump to next open message", 'n'
k.add :jump_to_prev_open, "Jump to previous open message", 'p'
+ k.add :align_current_message, "Align current message in buffer", 'z'
k.add :toggle_starred, "Star or unstar message", '*'
k.add :toggle_new, "Toggle unread/read status of message", 'N'
# k.add :collapse_non_new_messages, "Collapse all but unread messages", 'N'
@@ -274,27 +275,34 @@ EOS
end
end
- def jump_to_first_open
+ def jump_to_first_open loose_alignment=false
m = @message_lines[0] or return
if @layout[m].state != :closed
- jump_to_message m
+ jump_to_message m, loose_alignment
else
- jump_to_next_open
+ jump_to_next_open loose_alignment
end
end
- def jump_to_next_open
+ def jump_to_next_open loose_alignment=false
return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
- m = @message_lines[curpos] or return
+ m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
+ return unless m
while nextm = @layout[m].next
break if @layout[nextm].state != :closed
m = nextm
end
- jump_to_message nextm if nextm
+ jump_to_message nextm, loose_alignment if nextm
end
- def jump_to_prev_open
+ def align_current_message
m = @message_lines[curpos] or return
+ jump_to_message m
+ end
+
+ def jump_to_prev_open loose_alignment=false
+ m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a
+ return unless m
## jump to the top of the current message if we're in the body;
## otherwise, to the previous message
@@ -304,22 +312,32 @@ EOS
break if @layout[prevm].state != :closed
m = prevm
end
- jump_to_message prevm if prevm
+ jump_to_message prevm, loose_alignment if prevm
else
- jump_to_message m
+ jump_to_message m, loose_alignment
end
end
- def jump_to_message m
+ def jump_to_message m, loose_alignment=false
l = @layout[m]
left = l.depth * INDENT_SPACES
right = left + l.width
- ## jump to the top line unless both top and bottom fit in the current view
- jump_to_line l.top unless l.top >= topline && l.top <= botline && l.bot >= topline && l.bot <= botline
+ ## jump to the top line
+ if loose_alignment
+ jump_to_line [l.top - 3, 0].max # give 3 lines of top context
+ else
+ jump_to_line l.top
+ end
- ## jump to the left columns unless both left and right fit in the current view
- jump_to_col left unless left >= leftcol && left <= rightcol && right >= leftcol && right <= rightcol
+ ## jump to the left column
+ if loose_alignment
+ ## try and give 4 columns of left context, but not if it means that
+ ## the right of the message is truncated.
+ jump_to_col [[left - 4, rightcol - l.width - 1].min, 0].max
+ else
+ jump_to_col left
+ end
## either way, move the cursor to the first line
set_cursor_pos l.top
@@ -629,7 +647,7 @@ private
BufferManager.erase_flash
BufferManager.completely_redraw_screen
unless success
- BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s)
+ BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s, chunk.filename)
BufferManager.flash "Couldn't execute view command, viewing as text."
end
end