sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit c54156ce142289514bd45c3776cd2ce8ef4a8a00
parent 2ad87067a6190ab0c8626c1f6532cc0c003bbe15
Author: Gaute Hope <eg@gaute.vetsj.com>
Date:   Sun, 26 May 2013 13:22:25 +0200

Replace Iconv with built-ins for Ruby >=1.9

Diffstat:
M lib/sup/crypto.rb | 16 ++++++++--------
M lib/sup/message.rb | 8 +++++---
M lib/sup/message_chunks.rb | 2 +-
M lib/sup/rfc2047.rb | 2 +-
M lib/sup/util.rb | 83 +++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
5 files changed, 67 insertions(+), 44 deletions(-)
diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb
@@ -74,7 +74,7 @@ EOS
       end
 
     unless @gpgme_present
-      @not_working_reason = ['gpgme gem not present', 
+      @not_working_reason = ['gpgme gem not present',
         'Install the gpgme gem in order to use signed and encrypted emails']
       return
     end
@@ -85,7 +85,7 @@ EOS
     else
       # check if the gpg-options hook uses the passphrase_callback
       # if it doesn't then check if gpg agent is present
-      gpg_opts = HookManager.run("gpg-options", 
+      gpg_opts = HookManager.run("gpg-options",
                                {:operation => "sign", :options => {}}) || {}
       if gpg_opts[:passphrase_callback].nil?
         if ENV['GPG_AGENT_INFO'].nil?
@@ -116,7 +116,7 @@ EOS
 
     gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
     gpg_opts.merge!(gen_sign_user_opts(from))
-    gpg_opts = HookManager.run("gpg-options", 
+    gpg_opts = HookManager.run("gpg-options",
                                {:operation => "sign", :options => gpg_opts}) || gpg_opts
 
     begin
@@ -125,7 +125,7 @@ EOS
       raise Error, gpgme_exc_msg(exc.message)
     end
 
-    # if the key (or gpg-agent) is not available GPGME does not complain 
+    # if the key (or gpg-agent) is not available GPGME does not complain
     # but just returns a zero length string. Let's catch that
     if sig.length == 0
       raise Error, gpgme_exc_msg("GPG failed to generate signature: check that gpg-agent is running and your key is available.")
@@ -145,7 +145,7 @@ EOS
 
     gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
     if sign
-      gpg_opts.merge!(gen_sign_user_opts(from)) 
+      gpg_opts.merge!(gen_sign_user_opts(from))
       gpg_opts.merge!({:sign => true})
     end
     gpg_opts = HookManager.run("gpg-options",
@@ -158,7 +158,7 @@ EOS
       raise Error, gpgme_exc_msg(exc.message)
     end
 
-    # if the key (or gpg-agent) is not available GPGME does not complain 
+    # if the key (or gpg-agent) is not available GPGME does not complain
     # but just returns a zero length string. Let's catch that
     if cipher.length == 0
       raise Error, gpgme_exc_msg("GPG failed to generate cipher text: check that gpg-agent is running and your key is available.")
@@ -290,7 +290,7 @@ EOS
       # Look for Charset, they are put before the base64 crypted part
       charsets = payload.body.split("\n").grep(/^Charset:/)
       if !charsets.empty? and charsets[0] =~ /^Charset: (.+)$/
-        output = Iconv.easy_decode($encoding, $1, output)
+        output.transcode($encoding, $1)
       end
       msg.body = output
     else
@@ -362,7 +362,7 @@ private
       else
         first_sig = "Unknown error or empty signature"
       end
-    rescue EOFError 
+    rescue EOFError
       from_key = nil
       first_sig = "No public key available for #{signature.fingerprint}"
     end
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -69,7 +69,9 @@ class Message
     return unless v
     return v unless v.is_a? String
     return unless v.size < MAX_HEADER_VALUE_SIZE # avoid regex blowup on spam
-    Rfc2047.decode_to $encoding, Iconv.easy_decode($encoding, 'ASCII', v)
+    d = v.dup
+    d = d.transcode($encoding, 'ASCII')
+    Rfc2047.decode_to $encoding, d
   end
 
   def parse_header encoded_header
@@ -524,7 +526,7 @@ private
           ## if there's no charset, use the current encoding as the charset.
           ## this ensures that the body is normalized to avoid non-displayable
           ## characters
-          body = Iconv.easy_decode($encoding, m.charset || $encoding, m.decode)
+          body = m.decode.transcode($encoding, m.charset)
         else
           body = ""
         end
@@ -546,7 +548,7 @@ private
       msg = RMail::Message.new
       msg.body = gpg.join("\n")
 
-      body = Iconv.easy_decode(encoding_to, encoding_from, body)
+      body = body.transcode(encoding_to, encoding_from)
       lines = body.split("\n")
       sig = lines.between(GPG_SIGNED_START, GPG_SIG_START)
       startidx = lines.index(GPG_SIGNED_START)
diff --git a/lib/sup/message_chunks.rb b/lib/sup/message_chunks.rb
@@ -124,7 +124,7 @@ EOS
 
       @lines = nil
       if text
-        text = text.transcode(encoded_content.charset || $encoding)
+        text = text.transcode(encoded_content.charset || $encoding, text.encoding)
         @lines = text.gsub("\r\n", "\n").gsub(/\t/, "        ").gsub(/\r/, "").split("\n")
         @quotable = true
       end
diff --git a/lib/sup/rfc2047.rb b/lib/sup/rfc2047.rb
@@ -52,7 +52,7 @@ module Rfc2047
         # WORD.
       end
 
-      Iconv.easy_decode(target, charset, text)
+      text.transcode(target, charset)
     end
   end
 end
diff --git a/lib/sup/util.rb b/lib/sup/util.rb
@@ -5,7 +5,6 @@ require 'pathname'
 require 'set'
 require 'enumerator'
 require 'benchmark'
-require 'iconv'
 
 ## time for some monkeypatching!
 class Symbol
@@ -31,7 +30,7 @@ class Lockfile
   def dump_lock_id lock_id = @lock_id
       "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
         lock_id.values_at('host','pid','ppid','time','user', 'pname')
-    end
+  end
 
   def lockinfo_on_disk
     h = load_lock_id IO.read(path)
@@ -341,7 +340,57 @@ class String
     ret << s
   end
 
+  # Fix the damn string! make sure it is valid utf-8, then convert to
+  # user encoding.
+  #
+  # Not Ruby 1.8 compatible
+  def fix_encoding
+    if encoding == $encoding and valid_encoding?
+      self
+    end
+
+    encode!('UTF-8', :invalid => :replace, :undef => :replace)
+
+    unless valid_encoding?
+      encode!('UTF-16', 'UTF-8', :invalid => :replace, :undef => :replace)
+      encode!('UTF-8', 'UTF-16', :invalid => :replace, :undef => :replace)
+    end
+
+    fail "Could not create valid UTF-8 string out of: '#{self.to_s}'." unless valid_encoding?
+
+    # now convert to $encoding
+    if $encoding != 'UTF-8'
+      encode!($encoding, :invalid => :replace, :undef => :replace)
+    end
+
+    fail "Could not create valid #{$encoding.inspect?} string out of: '#{self.to_s}'." unless valid_encoding?
+
+    self
+  end
+
+  # transcode the string if original encoding is know
+  # fix if broken.
+  #
+  # Not Ruby 1.8 compatible
+  def transcode to_encoding, from_encoding
+    encode!(to_encoding, from_encoding, :invalid => :replace, :undef => :replace)
+
+    unless valid_encoding?
+      encode!('UTF-16', 'UTF-8', :invalid => :replace, :undef => :replace)
+      encode!('UTF-8', 'UTF-16', :invalid => :replace, :undef => :replace)
+    end
+
+    if to_encoding != 'UTF-8'
+      encode!(to_encoding, :invalid => :replace, :undef => :replace)
+    end
+
+    fail "Could not create valid #{to_encoding.inspect?} string out of: '#{self.to_s}'." unless valid_encoding?
+
+    self
+  end
+
   def normalize_whitespace
+    fix_encoding
     gsub(/\t/, "    ").gsub(/\r/, "")
   end
 
@@ -383,14 +432,10 @@ class String
         out << b.chr
       end
     end
-    out.force_encoding Encoding::UTF_8 if out.respond_to? :force_encoding
+    out.fix_encoding
     out
   end
 
-  def transcode src_encoding=$encoding
-    Iconv.easy_decode $encoding, src_encoding, self
-  end
-
   unless method_defined? :ascii_only?
     def ascii_only?
       size.times { |i| return false if self[i] & 128 != 0 }
@@ -659,27 +704,3 @@ class FinishLine
   end
 end
 
-class Iconv
-  def self.easy_decode target, orig_charset, text
-    if text.respond_to? :force_encoding
-      text = text.dup
-      text.force_encoding Encoding::BINARY
-    end
-    charset = case orig_charset
-      when /UTF[-_ ]?8/i then "utf-8"
-      when /(iso[-_ ])?latin[-_ ]?1$/i then "ISO-8859-1"
-      when /iso[-_ ]?8859[-_ ]?15/i then 'ISO-8859-15'
-      when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i then "utf-7"
-      when /^euc$/i then 'EUC-JP' # XXX try them all?
-      when /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i then 'ASCII'
-      else orig_charset
-    end
-
-    begin
-      returning(Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]) { |str| str.check }
-    rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::InvalidCharacter, Iconv::IllegalSequence, String::CheckError
-      debug "couldn't transcode text from #{orig_charset} (#{charset}) to #{target} (#{text[0 ... 20].inspect}...): got #{$!.class} (#{$!.message})"
-      text.ascii
-    end
-  end
-end