sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit e69c32cf9167851555361c01476e4181814d1bb3
parent 02e71b982e039fca40b98e4d6d4d3d8fa482240c
Author: Eric Weikl <eric.weikl@gmx.net>
Date:   Mon, 26 Aug 2013 10:25:11 +0200

Merge branch 'sup-heliotrope/develop' into maildir-sync

Diffstat:
M .gitignore | 4 ++++
M .travis.yml | 2 +-
M CONTRIBUTORS | 18 ++++++++++--------
M History.txt | 10 ++++++++++
M ReleaseNotes | 20 ++++++++++++++++++++
M bin/sup | 11 +++++------
M lib/sup.rb | 8 +++++---
M lib/sup/account.rb | 3 ++-
M lib/sup/buffer.rb | 21 ++++++++++++---------
M lib/sup/contact.rb | 4 +++-
M lib/sup/crypto.rb | 44 +++++++++++++++++++++++++++++++++++---------
M lib/sup/label.rb | 4 +++-
M lib/sup/message.rb | 4 ++--
M lib/sup/modes/compose_mode.rb | 2 ++
M lib/sup/modes/edit_message_mode.rb | 10 +++++-----
M lib/sup/modes/thread_view_mode.rb | 4 ++++
M lib/sup/poll.rb | 2 +-
M lib/sup/sent.rb | 10 ++++++----
M lib/sup/textfield.rb | 10 ++++++++++
M lib/sup/util.rb | 13 +++++++------
D release-script.txt | 17 -----------------
M sup.gemspec | 6 +++++-
A test/gnupg_test_home/gpg.conf | 1 +
A test/gnupg_test_home/pubring.gpg | 0
A test/gnupg_test_home/receiver_pubring.gpg | 0
A test/gnupg_test_home/receiver_secring.gpg | 0
A test/gnupg_test_home/receiver_trustdb.gpg | 0
A test/gnupg_test_home/secring.gpg | 0
A test/gnupg_test_home/sup-test-2@foo.bar.asc | 20 ++++++++++++++++++++
A test/gnupg_test_home/trustdb.gpg | 0
A test/test_crypto.rb | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
31 files changed, 282 insertions(+), 75 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -11,3 +11,7 @@ sup-exception-log.txt
 # bundler stuff
 Gemfile.lock
 .bundle
+
+# generated file for gnupg test
+test/gnupg_test_home/random_seed
+
diff --git a/.travis.yml b/.travis.yml
@@ -6,7 +6,7 @@ rvm:
 
 before_install:
   - sudo apt-get update -qq
-  - sudo apt-get install -qq uuid-dev uuid libncursesw5-dev libncursesw5
+  - sudo apt-get install -qq uuid-dev uuid libncursesw5-dev libncursesw5 gnupg2
 
 script: bundle exec rake travis
 
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
@@ -11,13 +11,14 @@ Eric Sherman <hyperbolist at the gmail dot coms>
 Tero Tilus <tero at the tilus dot nets>
 Ben Walton <bwalton at the artsci.utoronto dot cas>
 Mike Stipicevic <stipim at the rpi dot edus>
+Clint Byrum <clint at the ubuntu dot coms>
 Marcus Williams <marcus-sup at the bar-coded dot nets>
 Lionel Ott <white.magic at the gmx dot des>
 Gaudenz Steinlin <gaudenz at the soziologie dot chs>
 Damien Leone <damien.leone at the fensalir dot frs>
 Ingmar Vanhassel <ingmar at the exherbo dot orgs>
 Mark Alexander <marka at the pobox dot coms>
-Eric Weikl <eric.weikl at the tngtech dot coms>
+Eric Weikl <eric.weikl at the gmx dot nets>
 Christopher Warrington <chrisw at the rice dot edus>
 W. Trevor King <wking at the drexel dot edus>
 Richard Brown <rbrown at the exherbo dot orgs>
@@ -26,8 +27,8 @@ Marc Hartstein <marc.hartstein at the alum.vassar dot edus>
 Israel Herraiz <israel.herraiz at the gmail dot coms>
 Bo Borgerson <gigabo at the gmail dot coms>
 Michael Hamann <michael at the content-space dot des>
-William Erik Baxter <web at the superscript dot coms>
 Jonathan Lassoff <jof at the thejof dot coms>
+William Erik Baxter <web at the superscript dot coms>
 Grant Hollingworth <grant at the antiflux dot orgs>
 Markus Klinik <markus.klinik at the gmx dot des>
 Ico Doornekamp <ico at the pruts dot nls>
@@ -42,25 +43,26 @@ Decklin Foster <decklin at the red-bean dot coms>
 Cameron Matheson <cam+sup at the cammunism dot orgs>
 Carl Worth <cworth at the cworth dot orgs>
 Alex Vandiver <alex at the chmrr dot nets>
-Jeff Balogh <its.jeff.balogh at the gmail dot coms>
 Andrew Pimlott <andrew at the pimlott dot nets>
+Jeff Balogh <its.jeff.balogh at the gmail dot coms>
 Matías Aguirre <matiasaguirre at the gmail dot coms>
 Kornilios Kourtis <kkourt at the cslab.ece.ntua dot grs>
-Giorgio Lando <patroclo7 at the gmail dot coms>
 Kevin Riggle <kevinr at the free-dissociation dot coms>
+Giorgio Lando <patroclo7 at the gmail dot coms>
 Benoît PIERRE <benoit.pierre at the gmail dot coms>
+Matthieu Rakotojaona <matthieu.rakotojaona at the gmail dot coms>
 Alvaro Herrera <alvherre at the alvh.no-ip dot orgs>
 Steven Lawrance <stl at the koffein dot nets>
 Jonah <Jonah at the GoodCoffee dot cas>
 ian <itaylor at the uark dot edus>
-MichaelRevell <mikearevell at the gmail dot coms>
-Gregor Hoffleit <gregor at the sam.mediasupervision dot des>
 Todd Eisenberger <teisenbe at the andrew.cmu dot edus>
 Adam Lloyd <adam at the alloy-d dot nets>
+MichaelRevell <mikearevell at the gmail dot coms>
+Per Andersson <avtobiff at the gmail dot coms>
+Gregor Hoffleit <gregor at the sam.mediasupervision dot des>
 Steven Walter <swalter at the monarch.(none)>
 Jon M. Dugan <jdugan at the es dot nets>
-Matthieu Rakotojaona <matthieu.rakotojaona at the gmail dot coms>
 Matthias Vallentin <vallentin at the icir dot orgs>
 Stefan Lundström <lundst at the snabb.(none)>
-Whyme.Lyu <callme5long at the gmail dot coms>
+Horacio Sanson <horacio at the skillupjapan.co dot jps>
 Kirill Smelkov <kirr at the landau.phys.spbu dot rus>
diff --git a/History.txt b/History.txt
@@ -1,3 +1,13 @@
+== 0.14.0 / 2013-08-15
+
+* CJK compatability
+* Psych over Syck
+* Ruby 1.8 deprecated
+* Thread safety
+* No more Iconv, but using built in Ruby encodings. Better UTF-8
+  handling.
+* GPGME 2.0 support
+
 == 0.13.2 / 2013-06-26
 
 * FreeBSD 10 comptability
diff --git a/ReleaseNotes b/ReleaseNotes
@@ -1,3 +1,23 @@
+Release 0.14.0:
+
+CJK-compatability, Psych usage, thread safety, GPGME 2.0 support. Sup is now
+Ruby 1.9 based, and apart from RMail - ready for Ruby 2.0.0.
+
+Sup now uses Psych as a YAML parser (default by Ruby) and your previous
+configuration files (~/.sup/*.yaml) may need to be migrated or re-created for
+them to work with the new sup. A migration script is included for this.
+
+Check https://github.com/sup-heliotrope/sup/wiki/Migration-0.13-to-0.14 for
+the latest instructions.
+
+First back up your ~/.sup directory and index, after installing the new sup
+run:
+
+$ sup-psych-ify-config-files
+
+to migrate your files. You should now be all set for buisness.
+
+
 Release 0.13.2:
 
 FreeBSD compatability and more thread safe polling.
diff --git a/bin/sup b/bin/sup
@@ -1,4 +1,5 @@
 #!/usr/bin/env ruby
+# encoding: utf-8
 
 $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
 
@@ -8,8 +9,6 @@ require 'ncursesw'
 
 no_gpgme = false
 begin
-  # gpgme broke its API in 2.0, so make sure we have the old version for now.
-  gem 'gpgme', '=1.0.8'
   require 'gpgme'
 rescue LoadError
   no_gpgme = true
@@ -294,7 +293,7 @@ begin
       b.mode.load_in_background if new
     when :search
       completions = LabelManager.all_labels.map { |l| "label:#{LabelManager.string_for l}" }
-      completions = completions.each { |l| l.fix_encoding }
+      completions = completions.each { |l| l.fix_encoding! }
       completions += Index::COMPL_PREFIXES
       query = BufferManager.ask_many_with_completions :search, "Search all messages (enter for saved searches): ", completions
       unless query.nil?
@@ -308,7 +307,7 @@ begin
       SearchResultsMode.spawn_from_query "is:unread"
     when :list_labels
       labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
-      labels = labels.each { |l| l.fix_encoding }
+      labels = labels.each { |l| l.fix_encoding! }
 
       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
       unless user_label.nil?
@@ -376,7 +375,7 @@ ensure
     Index.stop_lock_update_thread
   end
 
-  HookManager.run "shutdown"
+  HookManager.run "shutdown" if HookManager.instantiated?
 
   Index.stop_sync_worker
   Redwood::finish
@@ -416,7 +415,7 @@ unless Redwood::exceptions.empty?
 We are very sorry. It seems that an error occurred in Sup. Please
 accept our sincere apologies. Please submit the contents of
 #{BASE_DIR}/exception-log.txt and a brief report of the
-circumstances to https://github.com/sup-heliotrope/sup/issues so that 
+circumstances to https://github.com/sup-heliotrope/sup/issues so that
 we might address this problem. Thank you!
 
 Sincerely,
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
 require 'rubygems'
 require 'yaml'
 YAML::ENGINE.yamler = 'psych'
@@ -302,7 +304,7 @@ EOM
     else
       require 'etc'
       require 'socket'
-      name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first rescue nil
+      name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first.force_encoding($encoding).fix_encoding! rescue nil
       name ||= ENV["USER"]
       email = ENV["USER"] + "@" +
         begin
@@ -314,8 +316,8 @@ EOM
       config = {
         :accounts => {
           :default => {
-            :name => name,
-            :email => email,
+            :name => name.dup.fix_encoding!,
+            :email => email.fix_encoding!,
             :alternates => [],
             :sendmail => "/usr/sbin/sendmail -oem -ti",
             :signature => File.join(ENV["HOME"], ".signature"),
diff --git a/lib/sup/account.rb b/lib/sup/account.rb
@@ -50,8 +50,9 @@ class AccountManager
       [:name, :sendmail, :signature, :gpgkey].each { |k| hash[k] ||= @default_account.send(k) }
     end
     hash[:alternates] ||= []
+    fail "alternative emails are not an array: #{hash[:alternates]}" unless hash[:alternates].kind_of? Array
 
-    [:name, :signature].each { |x| hash[x] ? hash[x].fix_encoding : nil }
+    [:name, :signature].each { |x| hash[x] ? hash[x].fix_encoding! : nil }
 
     a = Account.new hash
     @accounts[a] = true
diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
 require 'etc'
 require 'thread'
 
@@ -447,7 +449,7 @@ EOS
 
   def ask_with_completions domain, question, completions, default=nil
     ask domain, question, default do |s|
-      s.fix_encoding
+      s.fix_encoding!
       completions.select { |x| x =~ /^#{Regexp::escape s}/iu }.map { |x| [x, x] }
     end
   end
@@ -464,9 +466,9 @@ EOS
           raise "william screwed up completion: #{partial.inspect}"
         end
 
-      prefix.fix_encoding
-      target.fix_encoding
-      completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
+      prefix.fix_encoding!
+      target.fix_encoding!
+      completions.select { |x| x =~ /^#{Regexp::escape target}/iu }.map { |x| [prefix + x, x] }
     end
   end
 
@@ -474,12 +476,12 @@ EOS
     ask domain, question, default do |partial|
       prefix, target = partial.split_on_commas_with_remainder
       target ||= prefix.pop || ""
-      target.fix_encoding
+      target.fix_encoding!
 
       prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
-      prefix.fix_encoding
+      prefix.fix_encoding!
 
-      completions.select { |x| x =~ /^#{Regexp::escape target}/i }.sort_by { |c| [ContactManager.contact_for(c) ? 0 : 1, c] }.map { |x| [prefix + x, x] }
+      completions.select { |x| x =~ /^#{Regexp::escape target}/iu }.sort_by { |c| [ContactManager.contact_for(c) ? 0 : 1, c] }.map { |x| [prefix + x, x] }
     end
   end
 
@@ -492,7 +494,7 @@ EOS
         if dir
           [[s.sub(full, dir), "~#{name}"]]
         else
-          users.select { |u| u =~ /^#{Regexp::escape name}/ }.map do |u|
+          users.select { |u| u =~ /^#{Regexp::escape name}/u }.map do |u|
             [s.sub("~#{name}", "~#{u}"), "~#{u}"]
           end
         end
@@ -551,6 +553,7 @@ EOS
 
     completions = (recent + contacts).flatten.uniq
     completions += HookManager.run("extra-contact-addresses") || []
+
     answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
 
     if answer
@@ -619,7 +622,7 @@ EOS
       tf.deactivate
       draw_screen :sync => false, :status => status, :title => title
     end
-    tf.value.tap { |x| x.fix_encoding if x }
+    tf.value.tap { |x| x }
   end
 
   def ask_getch question, accept=nil
diff --git a/lib/sup/contact.rb b/lib/sup/contact.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
 module Redwood
 
 class ContactManager
@@ -54,7 +56,7 @@ class ContactManager
   def is_aliased_contact? person; !@p2a[person].nil? end
 
   def save
-    File.open(@fn, "w") do |f|
+    File.open(@fn, "w:UTF-8") do |f|
       @p2a.sort_by { |(p, a)| [p.full_address, a] }.each do |(p, a)|
         f.puts "#{a || ''}: #{p.full_address}"
       end
diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb
@@ -1,6 +1,4 @@
 begin
-  # gpgme broke its API in 2.0, so make sure we have the old version for now.
-  gem 'gpgme', '=1.0.8'
   require 'gpgme'
 rescue LoadError
 end
@@ -64,10 +62,17 @@ EOS
     @gpgme_present =
       begin
         begin
-          GPGME.check_version({:protocol => GPGME::PROTOCOL_OpenPGP})
+          begin
+            GPGME.check_version({:protocol => GPGME::PROTOCOL_OpenPGP})
+          rescue TypeError
+            GPGME.check_version(nil)
+          end
           true
         rescue GPGME::Error
           false
+        rescue ArgumentError
+          # gpgme 2.0.0 raises this due to the hash->string conversion
+          false
         end
       rescue NameError
         false
@@ -81,7 +86,11 @@ EOS
 
     # if gpg2 is available, it will start gpg-agent if required
     if (bin = `which gpg2`.chomp) =~ /\S/
-      GPGME.set_engine_info GPGME::PROTOCOL_OpenPGP, bin, nil
+      if GPGME.respond_to?('set_engine_info')
+        GPGME.set_engine_info GPGME::PROTOCOL_OpenPGP, bin, nil
+      else
+        GPGME.gpgme_set_engine_info GPGME::PROTOCOL_OpenPGP, bin, nil
+      end
     else
       # check if the gpg-options hook uses the passphrase_callback
       # if it doesn't then check if gpg agent is present
@@ -110,6 +119,7 @@ EOS
   end
 
   def have_crypto?; @not_working_reason.nil? end
+  def not_working_reason; @not_working_reason end
 
   def sign from, to, payload
     return unknown_status(@not_working_reason) unless @not_working_reason.nil?
@@ -120,7 +130,13 @@ EOS
                                {:operation => "sign", :options => gpg_opts}) || gpg_opts
 
     begin
-      sig = GPGME.detach_sign(format_payload(payload), gpg_opts)
+      if GPGME.respond_to?('detach_sign')
+        sig = GPGME.detach_sign(format_payload(payload), gpg_opts)
+      else
+        crypto = GPGME::Crypto.new
+        gpg_opts[:mode] = GPGME::SIG_MODE_DETACH
+        sig = crypto.sign(format_payload(payload), gpg_opts).read
+      end
     rescue GPGME::Error => exc
       raise Error, gpgme_exc_msg(exc.message)
     end
@@ -153,7 +169,13 @@ EOS
     recipients = to + [from]
     recipients = HookManager.run("gpg-expand-keys", { :recipients => recipients }) || recipients
     begin
-      cipher = GPGME.encrypt(recipients, format_payload(payload), gpg_opts)
+      if GPGME.respond_to?('encrypt')
+        cipher = GPGME.encrypt(recipients, format_payload(payload), gpg_opts)
+      else
+        crypto = GPGME::Crypto.new
+        gpg_opts[:recipients] = recipients
+        cipher = crypto.encrypt(format_payload(payload), gpg_opts).read
+      end
     rescue GPGME::Error => exc
       raise Error, gpgme_exc_msg(exc.message)
     end
@@ -262,7 +284,11 @@ EOS
                                {:operation => "decrypt", :options => gpg_opts}) || gpg_opts
     ctx = GPGME::Ctx.new(gpg_opts)
     cipher_data = GPGME::Data.from_str(format_payload(payload))
-    plain_data = GPGME::Data.empty
+    if GPGME::Data.respond_to?('empty')
+      plain_data = GPGME::Data.empty
+    else
+      plain_data = GPGME::Data.empty!
+    end
     begin
       ctx.decrypt_verify(cipher_data, plain_data)
     rescue GPGME::Error => exc
@@ -314,7 +340,7 @@ EOS
       msg = RMail::Parser.read output
       if msg.header.content_type =~ %r{^multipart/} && !msg.multipart?
         output = "MIME-Version: 1.0\n" + output
-        output.fix_encoding
+        output.fix_encoding!
         msg = RMail::Parser.read output
       end
     end
@@ -330,7 +356,7 @@ private
 
   def gpgme_exc_msg msg
     err_msg = "Exception in GPGME call: #{msg}"
-    info err_msg
+    #info err_msg
     err_msg
   end
 
diff --git a/lib/sup/label.rb b/lib/sup/label.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
 module Redwood
 
 class LabelManager
@@ -77,7 +79,7 @@ class LabelManager
 
   def save
     return unless @modified
-    File.open(@fn, "w") { |f| f.puts @labels.keys.sort_by { |l| l.to_s } }
+    File.open(@fn, "w:UTF-8") { |f| f.puts @labels.keys.sort_by { |l| l.to_s } }
     @new_labels = {}
   end
 end
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -112,7 +112,7 @@ class Message
     end
 
     subj = header["subject"]
-    subj = subj ? subj.fix_encoding : nil
+    subj = subj ? subj.fix_encoding! : nil
     @subj = subj ? subj.gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
     @to = Person.from_address_list header["to"]
     @cc = Person.from_address_list header["cc"]
@@ -338,7 +338,7 @@ EOS
   end
 
   def indexable_chunks
-    chunks.select { |c| c.is_a? Chunk::Text }
+    chunks ? chunks.select { |c| c.is_a? Chunk::Text } : []
   end
 
   def indexable_subject
diff --git a/lib/sup/modes/compose_mode.rb b/lib/sup/modes/compose_mode.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
 module Redwood
 
 class ComposeMode < EditMessageMode
diff --git a/lib/sup/modes/edit_message_mode.rb b/lib/sup/modes/edit_message_mode.rb
@@ -178,7 +178,7 @@ EOS
   def handle_new_text header, body; end
 
   def edit_message_or_field
-    lines = DECORATION_LINES + @selectors.size
+    lines = (@selectors.empty? ? 0 : DECORATION_LINES) + @selectors.size
     if lines > curpos
       return
     elsif (curpos - lines) >= @header_lines.length
@@ -484,10 +484,10 @@ protected
       m = build_message date
 
       if HookManager.enabled? "sendmail"
-    if not HookManager.run "sendmail", :message => m, :account => acct
-          warn "Sendmail hook was not successful"
-          return false
-    end
+        if not HookManager.run "sendmail", :message => m, :account => acct
+              warn "Sendmail hook was not successful"
+              return false
+        end
       else
         IO.popen(acct.sendmail, "w") { |p| p.puts m }
         raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
diff --git a/lib/sup/modes/thread_view_mode.rb b/lib/sup/modes/thread_view_mode.rb
@@ -848,6 +848,10 @@ private
       else
         width = buffer.content_width
       end
+      # lines can apparently be both String and Array, convert to Array for map.
+      if lines.kind_of? String
+        lines = lines.lines.to_a
+      end
       lines = lines.map { |l| l.chomp.wrap width if l }.flatten
     end
     return lines
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -195,7 +195,7 @@ EOS
   ## labels and locations set correctly. The Messages are saved to or removed
   ## from the index after being yielded.
   def poll_from source, opts={}
-    debug "trying to acquiring poll lock for: #{source}.."
+    debug "trying to acquire poll lock for: #{source}.."
     if source.poll_lock.try_lock
       debug "lock acquired for: #{source}."
       begin
diff --git a/lib/sup/sent.rb b/lib/sup/sent.rb
@@ -25,11 +25,13 @@ class SentManager
   end
 
   def write_sent_message date, from_email, &block
-    debug "store the sent message (locking sent source..)"
-    @source.poll_lock.synchronize do
-      @source.store_message date, from_email, &block
+    ::Thread.new do
+      debug "store the sent message (locking sent source..)"
+      @source.poll_lock.synchronize do
+        @source.store_message date, from_email, &block
+      end
+      PollManager.poll_from @source
     end
-    PollManager.poll_from @source
   end
 end
 
diff --git a/lib/sup/textfield.rb b/lib/sup/textfield.rb
@@ -168,6 +168,16 @@ private
     else # trailing spaces
       v + (" " * (x - @question.length - v.length))
     end
+
+    # ncurses returns a ASCII-8BIT (binary) string, which
+    # bytes presumably are of current charset encoding. we force_encoding
+    # so that the char representation / string is tagged will be the
+    # system locale and also hopefully the terminal/input encoding. an
+    # incorrectly configured terminal encoding (not matching the system
+    # encoding) will produce erronous results, but will also do that for
+    # a log of other programs since it is impossible to detect which is
+    # which and what encoding the inputted byte chars are supposed to have.
+    v.force_encoding($encoding).fix_encoding!
   end
 
   def remove_extra_space
diff --git a/lib/sup/util.rb b/lib/sup/util.rb
@@ -125,7 +125,7 @@ module RMail
       class << self
         def parse(field)
           field = field.dup.to_s
-          field = field.fix_encoding.ascii
+          field = field.fix_encoding!.ascii
           if field =~ EXTRACT_FIELD_NAME_RE
             [ $1, $'.chomp ]
           else
@@ -256,7 +256,7 @@ end
 
 class String
   def display_length
-    @display_length ||= Unicode.width(self, false)
+    @display_length ||= Unicode.width(self.fix_encoding!, false)
   end
 
   def slice_by_display_length len
@@ -366,7 +366,8 @@ class String
   # user encoding.
   #
   # Not Ruby 1.8 compatible
-  def fix_encoding
+  def fix_encoding!
+    # first try to encode to utf-8 from whatever current encoding
     encode!('UTF-8', :invalid => :replace, :undef => :replace)
 
     # do this anyway in case string is set to be UTF-8, encoding to
@@ -401,7 +402,7 @@ class String
 
     rescue Encoding::ConverterNotFoundError
       debug "Encoding converter not found for #{from_encoding.inspect} or #{to_encoding.inspect}, fixing string: '#{self.to_s}', but expect weird characters."
-      fix_encoding
+      fix_encoding!
     end
 
     fail "Could not create valid #{to_encoding.inspect} string out of: '#{self.to_s}'." unless valid_encoding?
@@ -410,7 +411,7 @@ class String
   end
 
   def normalize_whitespace
-    fix_encoding
+    fix_encoding!
     gsub(/\t/, "    ").gsub(/\r/, "")
   end
 
@@ -452,7 +453,7 @@ class String
         out << b.chr
       end
     end
-    out = out.fix_encoding # this should now be an utf-8 string of ascii
+    out = out.fix_encoding! # this should now be an utf-8 string of ascii
                            # compat chars.
   end
 
diff --git a/release-script.txt b/release-script.txt
@@ -1,17 +0,0 @@
-Just a few simple steps to make a new release.
-
-vi History.txt
-vi ReleaseNotes
-vi www/index.html # and bump version number
-git rank-contributors -n -o > CONTRIBUTORS
-vi CONTRIBUTORS   # and merge
-vi www/index.html # and include CONTRIBUTORS
-# ... git add, commit, etc
-git checkout -b release-<releasename>
-vi lib/sup.rb bin/* # and bump version numbers in all files
-# ... git add, commit, etc
-rake gem
-rake tarball
-gem push pkg/<gem name> # now using gemcutter
-git publish-branch
-rake upload_webpage
diff --git a/sup.gemspec b/sup.gemspec
@@ -36,7 +36,10 @@ DESC
     s.license = 'GPL-2'
     # TODO: might want to add index migrating script here, too
     s.post_install_message = <<-EOF
-SUP: Please run `sup-psych-ify-config-files` to migrate from 0.13 to 0.14
+SUP: Please run `sup-psych-ify-config-files` to migrate from 0.13 to 0.14.
+
+SUP: Check https://github.com/sup-heliotrope/sup/wiki/Migration-0.13-to-0.14
+     for more detailed up-to-date instructions.
     EOF
     s.files = SUP_FILES
     s.executables = SUP_EXECUTABLES
@@ -58,5 +61,6 @@ SUP: Please run `sup-psych-ify-config-files` to migrate from 0.13 to 0.14
     s.add_development_dependency "rake"
     s.add_development_dependency "minitest", "~> 4.7"
     s.add_development_dependency "rr", "~> 1.0.5"
+    s.add_development_dependency "gpgme", ">= 2.0.2"
   end
 end
diff --git a/test/gnupg_test_home/gpg.conf b/test/gnupg_test_home/gpg.conf
@@ -0,0 +1 @@
+default-key 789E7011
diff --git a/test/gnupg_test_home/pubring.gpg b/test/gnupg_test_home/pubring.gpg
Binary files differ.
diff --git a/test/gnupg_test_home/receiver_pubring.gpg b/test/gnupg_test_home/receiver_pubring.gpg
Binary files differ.
diff --git a/test/gnupg_test_home/receiver_secring.gpg b/test/gnupg_test_home/receiver_secring.gpg
Binary files differ.
diff --git a/test/gnupg_test_home/receiver_trustdb.gpg b/test/gnupg_test_home/receiver_trustdb.gpg
Binary files differ.
diff --git a/test/gnupg_test_home/secring.gpg b/test/gnupg_test_home/secring.gpg
Binary files differ.
diff --git a/test/gnupg_test_home/sup-test-2@foo.bar.asc b/test/gnupg_test_home/sup-test-2@foo.bar.asc
@@ -0,0 +1,20 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v2.0.20 (GNU/Linux)
+
+mI0EUgi0fAEEAOLAcQW96NEUSB7YE/la8X56jGW5BMX3aAixOF8LvOwMBbUK1T+U
+0H2PGIrXVcYyHcPqWRpRahbsIAldBqzffPlzMa+aqJaB1xKkNruxSoIzwPdidZMe
+l0Dxz2FDsoXD0KPyWnAYhGmQyz2MFpZxu2tlYqvwWVW//XGnk/KHvIXbABEBAAG0
+PlN1cCBUZXN0IFJlY2VpdmVyIChUZXN0IHJlY2VpdmVyIGZvciBTdXApIDxzdXAt
+dGVzdC0yQGZvby5iYXI+iL8EEwECACkFAlIItHwCGwMFCQHhM4AHCwkIBwMCAQYV
+CAIJCgsEFgIDAQIeAQIXgAAKCRAsABl+cWpykMMVBADHkQPgTz0CqKKp3k+z3dbm
+ocmI4tYNn1dOkDQqyfoBTfs6L3g4j5OE2UrguntRYyg5oon+uO5d18CQ5dY0sCw/
+o5IwyzTrxI8IocbtZvBdSb+XjLndynGuIQoqaJq9i6n1V4klFHVOna8Q9JstLfRX
+H1d4xPhnvKcaDDx/NV3X/biNBFIItHwBBADBpb43MpkrUWlg7HWJ1ZfOlxnOxrJ3
+Gz9WFNV06UbcZEuFKA/vHRjM6gWzUn5903FLuCWu3eBrq5xQfWipbp187PmocpoG
+skJ6gosLs1fMYRBjv2VbG9xJVKdKJMjqZw5FUpXKAaHr8P9jN6g2STQrbeQ8CVUK
+h7zOWRXAXSKUgwARAQABiKUEGAECAA8FAlIItHwCGwwFCQHhM4AACgkQLAAZfnFq
+cpDV1QQAzcxFXznEX92DjWxWRC7gRHgIsQk9WJnDzjtnDjSWCp3H85qeTZGZrn9W
+NoneV/S5Y7K3Mkceh4rFaANQ3zx4b05y1LFt5N/lPwIe5VB0vcPumtZum2fSGfpK
+nTXvzelcWcm2aGyUSaWvOkntWKEEt1kB5Oq6EtZoRZLMzAxLd7s=
+=aKsV
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/test/gnupg_test_home/trustdb.gpg b/test/gnupg_test_home/trustdb.gpg
Binary files differ.
diff --git a/test/test_crypto.rb b/test/test_crypto.rb
@@ -0,0 +1,109 @@
+# tests for sup's crypto libs
+#
+# Copyright Clint Byrum <clint@ubuntu.com> 2011. All Rights Reserved.
+# Copyright Sup Developers                 2013.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+require 'test_helper'
+require 'sup'
+require 'stringio'
+require 'tmpdir'
+
+module Redwood
+
+class TestCryptoManager < ::Minitest::Unit::TestCase
+
+    def setup
+        @from_email = 'sup-test-1@foo.bar'
+        @to_email   = 'sup-test-2@foo.bar'
+        # Use test gnupg setup
+        @orig_gnupghome = ENV['GNUPGHOME']
+        ENV['GNUPGHOME'] = File.join(File.dirname(__FILE__), 'gnupg_test_home')
+
+        @path = Dir.mktmpdir
+        Redwood::HookManager.init File.join(@path, 'hooks')
+
+        am = {:default=> {:name => "test", :email=> 'sup-test-1@foo.bar'}}
+        Redwood::AccountManager.init am
+
+        Redwood::CryptoManager.init
+
+        if not CryptoManager.have_crypto?
+          warn "No crypto set up, crypto will not be tested. Reason: #{CryptoManager.not_working_reason}"
+        end
+    end
+
+    def teardown
+      CryptoManager.deinstantiate!
+      AccountManager.deinstantiate!
+      HookManager.deinstantiate!
+      FileUtils.rm_r @path
+
+      ENV['GNUPGHOME'] = @orig_gnupghome
+    end
+
+    def test_sign
+        if CryptoManager.have_crypto? then
+            signed = CryptoManager.sign @from_email,@to_email,"ABCDEFG"
+            assert_instance_of RMail::Message, signed
+            assert_equal "ABCDEFG", signed.body[0]
+            assert signed.body[1].body.length > 0 , "signature length must be > 0"
+            assert (signed.body[1].body.include? "-----BEGIN PGP SIGNATURE-----") , "Expecting PGP armored data"
+        end
+    end
+
+    def test_encrypt
+        if CryptoManager.have_crypto? then
+            encrypted = CryptoManager.encrypt @from_email, [@to_email], "ABCDEFG"
+            assert_instance_of RMail::Message, encrypted
+            assert (encrypted.body[1].body.include? "-----BEGIN PGP MESSAGE-----") , "Expecting PGP armored data"
+        end
+    end
+
+    def test_sign_and_encrypt
+        if CryptoManager.have_crypto? then
+            encrypted = CryptoManager.sign_and_encrypt @from_email, [@to_email], "ABCDEFG"
+            assert_instance_of RMail::Message, encrypted
+            assert (encrypted.body[1].body.include? "-----BEGIN PGP MESSAGE-----") , "Expecting PGP armored data"
+        end
+    end
+
+    def test_decrypt
+        if CryptoManager.have_crypto? then
+            encrypted = CryptoManager.encrypt @from_email, [@to_email], "ABCDEFG"
+            assert_instance_of RMail::Message, encrypted
+            assert_instance_of String, (encrypted.body[1].body)
+            decrypted = CryptoManager.decrypt encrypted.body[1], true
+            assert_instance_of Array, decrypted
+            assert_instance_of Chunk::CryptoNotice, decrypted[0]
+            assert_instance_of Chunk::CryptoNotice, decrypted[1]
+            assert_instance_of RMail::Message, decrypted[2]
+            assert_equal "ABCDEFG" , decrypted[2].body
+        end
+    end
+
+    def test_verify
+        if CryptoManager.have_crypto?
+            signed = CryptoManager.sign @from_email, @to_email, "ABCDEFG"
+            assert_instance_of RMail::Message, signed
+            assert_instance_of String, (signed.body[1].body)
+            CryptoManager.verify signed.body[0], signed.body[1], true
+        end
+    end
+end
+
+end