sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit 7a0d0da45ee6671d2bdd74f0917d3716aaebbb13
parent d934d6f817a4de0327e442de58de1d2b511b5182
Author: Damien Leone <damien.leone@fensalir.fr>
Date:   Mon, 28 Jun 2010 00:00:11 +0200

Synchronize local modifications back to a Maildir source

Add the "sync_back_to_maildir" option which is false by default. It
works by updating a mail's filename according to the its current
labels in sup.

This commit also adds the :forwarded and :replied hidden reserved
labels to bring a better Maildir support according to [0]. These
labels are automatically added when replying, forwarding or bouncing a
message.

Keep in mind that deleting a mail in sup will set its 'T' Maildir flag
and might be deleted from the disk by software such as offlineimap.

Only messages that have been modified after having applied this commit
will be synchronized.

[0] http://cr.yp.to/proto/maildir.html

Conflicts:
	lib/sup.rb

Diffstat:
M lib/sup.rb | 3 ++-
M lib/sup/index.rb | 19 +++++++++++++------
M lib/sup/label.rb | 4 ++--
M lib/sup/maildir.rb | 60 +++++++++++++++++++++++++++++++++++++-----------------------
M lib/sup/message.rb | 9 +++++++++
M lib/sup/modes/forward-mode.rb | 15 +++++++++++----
M lib/sup/modes/reply-mode.rb | 6 ++++++
M lib/sup/modes/thread-view-mode.rb | 2 ++
M lib/sup/poll.rb | 4 +---
9 files changed, 83 insertions(+), 39 deletions(-)
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -286,7 +286,8 @@ EOS
       :wrap_width => 0,
       :slip_rows => 0,
       :col_jump => 2,
-      :stem_language => "english"
+      :stem_language => "english",
+      :sync_back_to_maildir => false
     }
     if File.exists? filename
       config = Redwood::load_yaml_obj filename
diff --git a/lib/sup/index.rb b/lib/sup/index.rb
@@ -452,14 +452,19 @@ EOS
     query
   end
 
+  def save_message m
+    return unless m.dirty?
+    if @sync_worker
+      @sync_queue << m
+    else
+      update_message_state m
+    end
+    m.clear_dirty
+  end
+
   def save_thread t
     t.each_dirty_message do |m|
-      if @sync_worker
-        @sync_queue << m
-      else
-        update_message_state m
-      end
-      m.clear_dirty
+      save_message m
     end
   end
 
@@ -626,6 +631,8 @@ EOS
   end
 
   def sync_message m, overwrite
+    m.sync_back if $config[:sync_back_to_maildir] and m.source.is_a? Maildir
+
     doc = synchronize { find_doc(m.id) }
     existed = doc != nil
     doc ||= Xapian::Document.new
diff --git a/lib/sup/label.rb b/lib/sup/label.rb
@@ -5,10 +5,10 @@ class LabelManager
 
   ## labels that have special semantics. user will be unable to
   ## add/remove these via normal label mechanisms.
-  RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox, :attachment ]
+  RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox, :attachment, :forwarded, :replied ]
 
   ## labels that will typically be hidden from the user
-  HIDDEN_RESERVED_LABELS = [ :starred, :unread, :attachment ]
+  HIDDEN_RESERVED_LABELS = [ :starred, :unread, :attachment, :forwarded, :replied ]
 
   def initialize fn
     @fn = fn
diff --git a/lib/sup/maildir.rb b/lib/sup/maildir.rb
@@ -71,6 +71,21 @@ class Maildir < Source
     with_file_for(id) { |f| RMail::Parser.read f }
   end
 
+  def sync_back id, labels
+    flags = ""
+
+    ## Flags must be stored in ASCII order according to Maildir
+    ## documentation
+    flags += "D" if labels.member? :draft
+    flags += "F" if labels.member? :starred
+    flags += "P" if labels.member? :forwarded
+    flags += "R" if labels.member? :replied
+    flags += "S" unless labels.member? :unread
+    flags += "T" if labels.member? :deleted
+
+    maildir_mark_file id, flags
+  end
+
   def raw_header id
     ret = ""
     with_file_for(id) do |f|
@@ -149,7 +164,10 @@ class Maildir < Source
   def maildir_labels id
     (seen?(id) ? [] : [:unread]) +
       (trashed?(id) ?  [:deleted] : []) +
-      (flagged?(id) ? [:starred] : [])
+      (flagged?(id) ? [:starred] : []) +
+      (passed?(id) ? [:forwarded] : []) +
+      (replied?(id) ? [:replied] : []) +
+      (draft?(id) ? [:draft] : [])
   end
 
   def draft? id; maildir_data(id)[2].include? "D"; end
@@ -159,13 +177,6 @@ class Maildir < Source
   def seen? id; maildir_data(id)[2].include? "S"; end
   def trashed? id; maildir_data(id)[2].include? "T"; end
 
-  def mark_draft id; maildir_mark_file id, "D" unless draft? id; end
-  def mark_flagged id; maildir_mark_file id, "F" unless flagged? id; end
-  def mark_passed id; maildir_mark_file id, "P" unless passed? id; end
-  def mark_replied id; maildir_mark_file id, "R" unless replied? id; end
-  def mark_seen id; maildir_mark_file id, "S" unless seen? id; end
-  def mark_trashed id; maildir_mark_file id, "T" unless trashed? id; end
-
   def valid? id
     File.exists? File.join(@dir, id)
   end
@@ -192,21 +203,24 @@ private
     [($1 || id), ($2 || "2"), ($3 || "")]
   end
 
-  ## not thread-safe on msg
-  def maildir_mark_file msg, flag
-    orig_path = @ids_to_fns[msg]
-    orig_base, orig_fn = File.split(orig_path)
-    new_base = orig_base.slice(0..-4) + 'cur'
-    tmp_base = orig_base.slice(0..-4) + 'tmp'
-    md_base, md_ver, md_flags = maildir_data msg
-    md_flags += flag; md_flags = md_flags.split(//).sort.join.squeeze
-    new_path = File.join new_base, "#{md_base}:#{md_ver},#{md_flags}"
-    tmp_path = File.join tmp_base, "#{md_base}:#{md_ver},#{md_flags}"
-    File.link orig_path, tmp_path
-    File.unlink orig_path
-    File.link tmp_path, new_path
-    File.unlink tmp_path
-    @ids_to_fns[msg] = new_path
+  def maildir_mark_file orig_path, flags
+    @mutex.synchronize do
+      new_base = (flags.include?("S")) ? "cur" : "new"
+      md_base, md_ver, md_flags = maildir_data orig_path
+      return orig_path if md_flags == flags
+
+      new_loc = File.join new_base, "#{md_base}:#{md_ver},#{flags}"
+      orig_path = File.join @dir, orig_path
+      new_path  = File.join @dir, new_loc
+      tmp_path  = File.join @dir, "tmp", "#{md_base}:#{md_ver},#{flags}"
+
+      File.link orig_path, tmp_path
+      File.unlink orig_path
+      File.link tmp_path, new_path
+      File.unlink tmp_path
+
+      new_loc
+    end
   end
 end
 
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -285,6 +285,10 @@ EOS
     location.each_raw_message_line &b
   end
 
+  def sync_back
+    location.sync_back @labels
+  end
+
   ## returns all the content from a message that will be indexed
   def indexable_content
     load_from_source!
@@ -696,6 +700,11 @@ class Location
     source.raw_message info
   end
 
+  def sync_back labels
+    new_info = source.sync_back(@info, labels) if source.respond_to? :sync_back
+    @info = new_info if new_info
+  end
+
   ## much faster than raw_message
   def each_raw_message_line &b
     source.each_raw_message_line info, &b
diff --git a/lib/sup/modes/forward-mode.rb b/lib/sup/modes/forward-mode.rb
@@ -7,9 +7,10 @@ class ForwardMode < EditMessageMode
       "From" => AccountManager.default_account.full_address,
     }
 
+    @m = opts[:message]
     header["Subject"] =
-      if opts[:message]
-        "Fwd: " + opts[:message].subj
+      if @m
+        "Fwd: " + @m.subj
       elsif opts[:attachments]
         "Fwd: " + opts[:attachments].keys.join(", ")
       end
@@ -19,8 +20,8 @@ class ForwardMode < EditMessageMode
     header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
 
     body =
-      if opts[:message]
-        forward_body_lines(opts[:message])
+      if @m
+        forward_body_lines @m
       elsif opts[:attachments]
         ["Note: #{opts[:attachments].size.pluralize 'attachment'}."]
       end
@@ -68,6 +69,12 @@ protected
       m.quotable_header_lines + [""] + m.quotable_body_lines +
       ["--- End forwarded message ---"]
   end
+
+  def send_message
+    return unless super # super returns true if the mail has been sent
+    @m.add_label :forwarded
+    Index.save_message @m
+  end
 end
 
 end
diff --git a/lib/sup/modes/reply-mode.rb b/lib/sup/modes/reply-mode.rb
@@ -217,6 +217,12 @@ protected
       update
     end
   end
+
+  def send_message
+    return unless super # super returns true if the mail has been sent
+    @m.add_label :replied
+    Index.save_message @m
+  end
 end
 
 end
diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb
@@ -246,6 +246,8 @@ EOS
           sm.puts m.raw_message
         end
         raise SendmailCommandFailed, "Couldn't execute #{cmd}" unless $? == 0
+        m.add_label :forwarded
+        Index.save_message m
       rescue SystemCallError, SendmailCommandFailed => e
         warn "problem sending mail: #{e.message}"
         BufferManager.flash "Problem sending mail: #{e.message}"
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -209,9 +209,7 @@ EOS
             m.locations.delete Location.new(source, args[:old_info])
             m.locations.push Location.new(source, args[:new_info])
             ## Update labels that might have been modified remotely
-            [:unread, :starred, :deleted].each do |l|
-              m.labels.delete l
-            end
+            m.labels -= [:draft, :starred, :forwarded, :replied, :unread, :deleted]
             m.labels += args[:labels]
             yield :update, m
             Index.sync_message m, true