sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit 7a035e6a53449b77cceae1d8045c51c4063b0ab6
parent 4255437a5b252c7268c7ec2743b182982334ccb5
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sun, 12 May 2024 14:07:11 +1000

use a fallback date if Date header is missing

If a message doesn't have a usable Date header, we can fall back to
using the modtime of the file for Maildir, or the delivery time which
should have been written to the From line in mbox.

If all else fails, use a fixed date in the past, rather than Time.now.
This will prevent the mails from popping back to the top of the thread
list every time Sup reloads them.

Fixes #596.

Diffstat:
M lib/sup/maildir.rb | 4 ++++
M lib/sup/mbox.rb | 21 +++++++++++++++++++++
M lib/sup/message.rb | 16 +++++++---------
M test/dummy_source.rb | 4 ++++
M test/integration/test_maildir.rb | 16 +++++++++++++++-
M test/integration/test_mbox.rb | 10 ++++++++++
6 files changed, 61 insertions(+), 10 deletions(-)
diff --git a/lib/sup/maildir.rb b/lib/sup/maildir.rb
@@ -114,6 +114,10 @@ class Maildir < Source
     with_file_for(id) { |f| f.read }
   end
 
+  def fallback_date_for_message id
+    File.mtime File.join(@dir, id)
+  end
+
   ## XXX use less memory
   def poll
     added = []
diff --git a/lib/sup/mbox.rb b/lib/sup/mbox.rb
@@ -1,5 +1,6 @@
 require 'uri'
 require 'set'
+require 'time'
 
 module Redwood
 
@@ -137,6 +138,26 @@ class MBox < Source
     end
   end
 
+  def fallback_date_for_message offset
+    ## This is a bit awkward... We treat the From line as a delimiter,
+    ## not part of the message. So the offset is pointing *after* the
+    ## From line for the desired message. With a bit of effort we can
+    ## scan backwards to find its From line and extract a date from it.
+    buf = @mutex.synchronize do
+      ensure_open
+      start = offset
+      loop do
+        start = (start - 200).clamp 0, 2**64
+        @f.seek start
+        buf = @f.read (offset - start)
+        break buf if buf.include? ?\n or start == 0
+      end
+    end
+    BREAK_RE.match buf.lines.last do |m|
+      Time.strptime m[1], "%a %b %d %H:%M:%S %Y"
+    end
+  end
+
   def default_labels
     [:inbox, :unread]
   end
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -103,16 +103,10 @@ class Message
     when Time
       date
     when String
-      begin
-        Time.parse date
-      rescue ArgumentError
-        #debug "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
-        Time.now
-      end
-    else
-      #debug "faking non-existent date header for #{@id}"
-      Time.now
+      Time.parse date rescue nil
     end
+    @date = location.fallback_date if @date.nil?
+    @date = Time.utc 1970, 1, 1 if @date.nil?
 
     subj = header["subject"]
     subj = subj ? subj.fix_encoding! : nil
@@ -808,6 +802,10 @@ class Location
     source.load_message info
   end
 
+  def fallback_date
+    source.fallback_date_for_message info
+  end
+
   def valid?
     source.valid? info
   end
diff --git a/test/dummy_source.rb b/test/dummy_source.rb
@@ -59,6 +59,10 @@ class DummySource < Source
       end
     end
   end
+
+  def fallback_date_for_message id
+    Time.utc 2001, 2, 3, 4, 56, 57
+  end
 end
 
 end
diff --git a/test/integration/test_maildir.rb b/test/integration/test_maildir.rb
@@ -34,7 +34,9 @@ EOS
   end
 
   def create_a_maildir_email(folder, content)
-    File.write(File.join(folder, "#{Time.now.to_f}.hostname:2,S"), content)
+    filename = File.join folder, "#{Time.now.to_f}.hostname:2,S"
+    File.write filename, content
+    filename
   end
 
   def start_sup_and_add_source(source)
@@ -74,5 +76,17 @@ EOS
 
   end
 
+  def test_missing_date_header
+    ## The message is missing a Date header so we should use its modtime
+    ## as a fallback.
+    fallback_date = Time.new 2004, 4, 19, 11, 12, 13
+    maildir = create_a_maildir
+    filename = create_a_maildir_email(File.join(maildir, 'cur'), @test_message_1)
+    File.utime fallback_date, fallback_date, filename
+    start_sup_and_add_source Maildir.new "maildir:#{maildir}"
+
+    messages_in_index = Index.instance.enum_for(:each_message).to_a
+    assert_equal fallback_date, messages_in_index.first.date
+  end
 end
 
diff --git a/test/integration/test_mbox.rb b/test/integration/test_mbox.rb
@@ -69,4 +69,14 @@ EOS
 
   end
 
+  def test_missing_date_header
+    ## The message is missing a Date header so we should use envelope date
+    ## stored in the From line as a fallback.
+    fallback_date = Time.new 2009, 4, 27, 12, 56, 18
+    mbox = create_a_mbox
+    start_sup_and_add_source MBox.new "mbox:#{mbox}"
+
+    messages_in_index = Index.instance.enum_for(:each_message).to_a
+    assert_equal fallback_date, messages_in_index.first.date
+  end
 end