[DRE-commits] [schleuder] 03/13: New upstream version 3.2.0

Georg Faerber georg-alioth-guest at moszumanska.debian.org
Tue Oct 24 12:31:00 UTC 2017


This is an automated email from the git hooks/post-receive script.

georg-alioth-guest pushed a commit to branch debian/3.2
in repository schleuder.

commit 6ac52b1ef90e21c74154d4605b58418c3da9bd5b
Author: Georg Faerber <georg at riseup.net>
Date:   Mon Oct 23 11:50:14 2017 +0200

    New upstream version 3.2.0
---
 .gitattributes                                     |   2 +
 .gitignore                                         |   1 +
 .travis.yml                                        |   1 +
 CHANGELOG.md                                       |  28 ++
 Gemfile.lock                                       |  34 +-
 README.md                                          |   8 +-
 bin/schleuder-api-daemon                           | 359 +-------------
 db/migrate/20140501103532_create_lists.rb          |  64 +--
 db/migrate/20140501112859_create_subscriptions.rb  |  26 +-
 db/migrate/201508092100_add_language_to_lists.rb   |  10 +-
 ...813235800_add_forward_all_incoming_to_admins.rb |  10 +-
 .../201508222143_add_logfiles_to_keep_to_lists.rb  |  10 +-
 ...abled_to_delivery_enabled_and_change_default.rb |   6 +-
 db/migrate/201508261815_strip_gpg_passphrase.rb    |   4 +-
 .../20170713215059_add_internal_footer_to_list.rb  |  11 +
 db/schema.rb                                       |   3 +-
 etc/list-defaults.yml                              |   7 +-
 etc/schleuder.yml                                  |   3 +
 .../schleuder-api-daemon.rb                        |  93 +++-
 lib/schleuder/conf.rb                              |   4 +-
 lib/schleuder/gpgme/ctx.rb                         |  49 +-
 lib/schleuder/gpgme/key.rb                         |  35 ++
 lib/schleuder/list.rb                              |  55 ++-
 lib/schleuder/list_builder.rb                      |  30 +-
 lib/schleuder/mail/message.rb                      |  48 +-
 lib/schleuder/plugin_runners/base.rb               |   4 +-
 lib/schleuder/plugins/key_management.rb            |   5 +-
 lib/schleuder/plugins/resend.rb                    |  44 +-
 lib/schleuder/plugins/sign_this.rb                 |   3 +
 lib/schleuder/plugins/subscription_management.rb   |  19 +-
 lib/schleuder/version.rb                           |   2 +-
 locales/de.yml                                     |  23 +-
 locales/en.yml                                     |  23 +-
 schleuder.gemspec                                  |   5 +-
 spec/factories/lists.rb                            |   1 +
 spec/fixtures/expiring_key.txt                     |  30 ++
 spec/fixtures/mails/qp-encoding-clear.eml          |  25 +
 .../mails/qp-encoding-encrypted+signed.eml         |  65 +++
 spec/fixtures/mails/qp-encoding-encrypted.eml      |  47 ++
 spec/fixtures/partially_expired_key.txt            |  36 ++
 spec/fixtures/revoked_key.txt                      |  33 ++
 spec/fixtures/schleuder_at_example_public_key.txt  |   2 +
 spec/fixtures/signonly_key.txt                     |  18 +
 spec/schleuder-certificate.pem                     |  21 +
 spec/schleuder-private-key.pem                     |  27 +
 spec/schleuder.yml                                 |  10 +-
 .../api_daemon/api_daemon_spec_helper.rb           |  17 +
 .../integration/api_daemon/authorization_spec.rb   |  20 +
 spec/schleuder/integration/api_daemon/list_spec.rb |  29 ++
 .../integration/api_daemon/subscription_spec.rb    |  68 +++
 spec/schleuder/integration/cli_spec.rb             |   5 +-
 spec/schleuder/integration/keywords_spec.rb        | 547 ++++++++++++++++++---
 spec/schleuder/runner_spec.rb                      |  83 ++++
 spec/schleuder/unit/conf_spec.rb                   |  38 ++
 spec/schleuder/unit/filters_spec.rb                |   2 +-
 spec/schleuder/unit/gpgme_ctx.rb                   |  22 +
 spec/schleuder/unit/gpgme_key.rb                   |  50 ++
 spec/schleuder/unit/list_builder_spec.rb           |  42 +-
 spec/schleuder/unit/list_spec.rb                   | 218 +++++++-
 spec/schleuder/unit/logger_notifications_spec.rb   |  29 ++
 spec/schleuder/unit/message_spec.rb                |  24 +
 spec/spec_helper.rb                                |  34 +-
 62 files changed, 1959 insertions(+), 613 deletions(-)

diff --git a/.gitattributes b/.gitattributes
index d547568..48142d7 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,3 @@
+.gitlab-ci.yml export-ignore
 /gems/ export-ignore
+/utils export-ignore
diff --git a/.gitignore b/.gitignore
index fde347c..4294f3b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ vendor
 db/*.sqlite3
 testmails/*
 .pc/
+coverage/
diff --git a/.travis.yml b/.travis.yml
index 6801136..1f934b2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,4 @@
+dist: trusty
 language: ruby
 cache: bundler
 rvm:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c60a57..33c464b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,34 @@ Change Log
 
 This project adheres to [Semantic Versioning](http://semver.org/).
 
+## [3.2.0] / 2017-10-23
+
+### Added
+
+* Internal footer: to be appended to each email that is sent to a subscribed address. Will not be included in messages to non-subscribed addresses. This change requires a change to the database, don't forget to run `schleuder install` after updating the code.
+* Optionally use an OS-wide defined keyserver by configuring a blank value for the keyserver.
+* Added keywords `X-RESEND-UNENCRYPTED` and `X-RESEND-CC-UNENCRYPTED` to enforce outgoing email(s) in cleartext regardless of whether we would find a key for the recipient or not.
+
+
+### Changed
+
+* Public footer: Whitespace is not anymore stripped from the value of public_footer.
+* The API does not include anymore each key's key-data in response to `/keys.json`. This avoids performance problems with even medium sized keyrings.
+* The short representation of GnuPG keys became more human-friendly. Besides the fingerprint we now show the email-address of the first UID, the generation-date, and optionally the expiration-date.
+* Log the full exception when sending a message fails. (Thanks, Lunar!)
+* When creating a new list, we do not anymore look for a matching key for the admin-address in the list's keyring. We don't want to look up keys for subscriptions by email at all. (This was anyway only useful in the corner case where you prefilled a keyring to use for the new list.)
+* API: Access to `/status.json` is now allowed without authentication.
+* Deprecate X-LISTNAME in favour of X-LIST-NAME, for the sake of consistency in spelling keywords (but X-LISTNAME is still supported). (Thanks, maxigas!)
+
+### Fixed
+
+* X-SUBSCRIBE now handles the combination of space-separated fingerprint and additional arguments (admin-flag, delivery-enabled-flag) correctly.
+* Fixed broken encoding of certain character-sequences in encrypted+signed messages.
+* X-LIST-KEYS again works without arguments.
+* X-RESEND now checks the given arguments to be valid email-addresses, and blocks resending if any one is found invalid.
+* X-RESEND now respects the encoding the mail was sent with. (Thanks, Lunar!)
+
+
 ## [3.1.2] / 2017-07-13
 
 ### Changed
diff --git a/Gemfile.lock b/Gemfile.lock
index 47d725b..c990c2a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,10 +1,10 @@
 PATH
   remote: .
   specs:
-    schleuder (3.1.2)
+    schleuder (3.2.0)
       activerecord (~> 4.1)
       mail-gpg (~> 0.3.0)
-      rake (~> 12)
+      rake (>= 10.5.0)
       sinatra (~> 1)
       sinatra-contrib (~> 1)
       sqlite3 (~> 1)
@@ -27,19 +27,21 @@ GEM
       minitest (~> 5.1)
       thread_safe (~> 0.3, >= 0.3.4)
       tzinfo (~> 1.1)
+    ansi (1.5.0)
     arel (6.0.4)
     backports (3.6.8)
     builder (3.2.2)
     daemons (1.2.4)
     database_cleaner (1.5.3)
     diff-lcs (1.2.5)
+    docile (1.1.5)
     eventmachine (1.2.3)
     factory_girl (4.8.0)
       activesupport (>= 3.0.0)
-    gpgme (2.0.12)
-      mini_portile2 (~> 2.1.0)
+    gpgme (2.0.13)
+      mini_portile2 (~> 2.1)
     hirb (0.7.3)
-    i18n (0.7.0)
+    i18n (0.8.1)
     json (1.8.5)
     mail (2.6.4)
       mime-types (>= 1.16, < 4)
@@ -49,10 +51,10 @@ GEM
     mime-types (3.1)
       mime-types-data (~> 3.2015)
     mime-types-data (3.2016.0521)
-    mini_portile2 (2.1.0)
+    mini_portile2 (2.2.0)
     minitest (5.10.1)
     multi_json (1.12.1)
-    rack (1.6.5)
+    rack (1.6.8)
     rack-protection (1.5.3)
       rack
     rack-test (0.6.3)
@@ -71,7 +73,16 @@ GEM
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.5.0)
     rspec-support (3.5.0)
-    sinatra (1.4.7)
+    simplecov (0.15.1)
+      docile (~> 1.1.0)
+      json (>= 1.8, < 3)
+      simplecov-html (~> 0.10.0)
+    simplecov-console (0.4.2)
+      ansi
+      hirb
+      simplecov
+    simplecov-html (0.10.2)
+    sinatra (1.4.8)
       rack (~> 1.5)
       rack-protection (~> 1.4)
       tilt (>= 1.3, < 3)
@@ -88,8 +99,8 @@ GEM
       eventmachine (~> 1.0, >= 1.0.4)
       rack (>= 1, < 3)
     thor (0.19.4)
-    thread_safe (0.3.5)
-    tilt (2.0.5)
+    thread_safe (0.3.6)
+    tilt (2.0.8)
     tzinfo (1.2.2)
       thread_safe (~> 0.1)
 
@@ -102,6 +113,7 @@ DEPENDENCIES
   hirb
   rspec (~> 3.5.0)
   schleuder!
+  simplecov-console
 
 BUNDLED WITH
-   1.14.6
+   1.13.7
diff --git a/README.md b/README.md
index 6a229c3..705e696 100644
--- a/README.md
+++ b/README.md
@@ -43,15 +43,15 @@ Additionally these **rubygems** are required (will be installed automatically un
 Installing Schleuder
 ------------
 
-1. Download [the gem](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.1.2.gem) and [the OpenPGP-signature](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.1.2.gem.sig) and verify:
+1. Download [the gem](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.2.0.gem) and [the OpenPGP-signature](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.2.0.gem.sig) and verify:
    ```
    gpg --recv-key 0xB3D190D5235C74E1907EACFE898F2C91E2E6E1F3
-   gpg --verify schleuder-3.1.2.gem.sig
+   gpg --verify schleuder-3.2.0.gem.sig
    ```
 
 2. If all went well install the gem:
    ```
-   gem install schleuder-3.1.2.gem
+   gem install schleuder-3.2.0.gem
    ```
 
 3. Set up schleuder:
@@ -131,4 +131,4 @@ GNU GPL 3.0. Please see [LICENSE.txt](LICENSE.txt).
 Alternative Download
 --------------------
 
-Alternatively to the gem-files you can download the latest release as [a tarball](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.1.2.tar.gz) and [its OpenPGP-signature](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.1.2.tar.gz.sig).
+Alternatively to the gem-files you can download the latest release as [a tarball](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.2.0.tar.gz) and [its OpenPGP-signature](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.2.0.tar.gz.sig).
diff --git a/bin/schleuder-api-daemon b/bin/schleuder-api-daemon
index 80eaeaf..8e81a82 100755
--- a/bin/schleuder-api-daemon
+++ b/bin/schleuder-api-daemon
@@ -1,359 +1,4 @@
 #!/usr/bin/env ruby
 
-# Make sinatra use production as default-environment
-ENV['RACK_ENV'] ||= 'production'
-
-require 'sinatra/base'
-require 'sinatra/json'
-require 'sinatra/namespace'
-require 'thin'
-require_relative '../lib/schleuder.rb'
-
-
-%w[tls_cert_file tls_key_file].each do |config_key|
-  path = Conf.api[config_key]
-  if ! File.readable?(path)
-    $stderr.puts "Error: '#{path}' is not a readable file (from #{config_key} in config)."
-    exit 1
-  end
-end
-
-class SchleuderApiDaemon < Sinatra::Base
-  register Sinatra::Namespace
-  use Rack::Auth::Basic, "Schleuder API Daemon" do |username, key|
-    username == 'schleuder' && Conf.api_valid_api_keys.include?(key)
-  end
-
-  configure do
-    set :server, :thin
-    set :port, Schleuder::Conf.api['port'] || 4443
-    set :bind, Schleuder::Conf.api['host'] || 'localhost'
-    if settings.development?
-      set :logging, Logger::DEBUG
-    else
-      set :logging, Logger::WARN
-    end
-  end
-
-  before do
-    cast_param_values
-  end
-
-  after do
-    # Return connection to pool after each request.
-    ActiveRecord::Base.connection.close
-  end
-
-  error do
-    exc = env['sinatra.error']
-    logger.error "Error: #{env['sinatra.error'].message}"
-    case exc
-    when Errno::EACCES
-      server_error(exc.message)
-    else
-      client_error(exc.to_s)
-    end
-  end
-
-  error 404 do
-    'Not found'
-  end
-
-  get '/status.json' do
-    json status: :ok
-  end
-
-  get '/version.json' do
-    json version: Schleuder::VERSION
-  end
-
-  helpers do
-    def list(id_or_email=nil)
-      if id_or_email.blank?
-        if params[:list_id].present?
-          id_or_email = params[:list_id]
-        else
-          client_error "Parameter list_id is required"
-        end
-      end
-      if id_or_email.to_i == 0
-        # list_id is actually an email address
-        list = List.where(email: id_or_email).first
-      else
-        list = List.where(id: id_or_email).first
-      end
-      list || halt(404)
-    end
-
-    def subscription(id_or_email)
-      if id_or_email.to_i == 0
-        # Email
-        if params[:list_id].blank?
-          client_error "Parameter list_id is required when using email as identifier for subscriptions."
-        else
-          sub = list.subscriptions.where(email: id_or_email).first
-        end
-      else
-        sub = Subscription.where(id: id_or_email.to_i).first
-      end
-      sub || halt(404)
-    end
-
-    def requested_list_id
-      # ActiveResource doesn't want to use query-params with create(), so here
-      # list_id might be included in the request-body.
-      params['list_id'] || parsed_body['list_id'] || client_error('Need list_id')
-    end
-
-    def parsed_body
-      @parsed_body ||= begin
-          b = JSON.parse(request.body.read)
-          logger.debug "parsed body: #{b.inspect}"
-          b
-        end
-    end
-
-    def server_error(msg)
-      logger.warn msg
-      halt(500, json(error: msg))
-    end
-
-    # TODO: unify error messages. This method currently sends an old error format. See <https://github.com/rails/activeresource/blob/d6a5186/lib/active_resource/base.rb#L227>.
-    def client_error(obj_or_msg, http_code=400)
-      text = case obj_or_msg
-             when String, Symbol
-               obj_or_msg.to_s
-             when ActiveRecord::Base
-               obj_or_msg.errors.full_messages
-             else
-               obj_or_msg
-             end
-      logger.error "Sending error to client: #{text.inspect}"
-      halt(http_code, json(errors: text))
-    end
-
-    # poor persons type casting
-    def cast_param_values
-      params.each do |key, value|
-        params[key] =
-            case value
-            when 'true' then true
-            when 'false' then false
-            when '0' then 0
-            when value.to_i > 0 then value.to_i
-            else value
-            end
-      end
-    end
-
-    def key_to_json(key)
-      {
-        fingerprint: key.fingerprint,
-        email: key.email,
-        ascii: key.armored,
-        expiry: key.expires,
-        trust_issues: key.usability_issue
-      }
-    end
-  end
-
-  namespace '/lists' do
-    get '.json' do
-      json List.all, include: :subscriptions
-    end
-
-    post '.json' do
-      listname = parsed_body['email']
-      fingerprint = parsed_body['fingerprint']
-      adminaddress = parsed_body['adminaddress']
-      adminkey = parsed_body['adminkey']
-      list, messages = ListBuilder.new({email: listname, fingerprint: fingerprint}, adminaddress, adminkey).run
-      if list.nil?
-        client_error(messages, 422)
-      elsif ! list.valid?
-        client_error(list, 422)
-      else
-        if messages.present?
-          headers 'X-Messages' => messages.join(' // ').gsub(/\n/, ' // ')
-        end
-        body json(list)
-      end
-    end
-
-    get '/configurable_attributes.json' do
-      json(List.configurable_attributes) + "\n"
-    end
-
-    post '/send_list_key_to_subscriptions.json' do
-      json(result: list.send_list_key_to_subscriptions)
-    end
-
-    get '/new.json' do
-      json List.new
-    end
-
-    get '/:id.json' do |id|
-      json list(id)
-    end
-
-    put '/:id.json' do |id|
-      list = list(id)
-      if list.update(parsed_body)
-        204
-      else
-        client_error(list)
-      end
-    end
-
-    patch '/:id.json' do |id|
-      list = list(id)
-      if list.update(parsed_body)
-        204
-      else
-        client_error(list)
-      end
-    end
-
-    delete '/:id.json' do |id|
-      list = list(id)
-      if list.destroy
-        200
-      else
-        client_error(list)
-      end
-    end
-  end
-
-  namespace '/subscriptions' do
-    get '.json' do
-      filterkeys = Subscription.configurable_attributes + [:list_id, :email]
-      filter = params.select do |param|
-        filterkeys.include?(param.to_sym)
-      end
-
-      logger.debug "Subscription filter: #{filter.inspect}"
-      if filter['list_id'] && filter['list_id'].to_i == 0
-        # Value is an email-address
-        if list = List.where(email: filter['list_id']).first
-          filter['list_id'] = list.id
-        else
-          status 404
-          return json(errors: 'No such list')
-        end
-      end
-
-      json Subscription.where(filter)
-    end
-
-    post '.json' do
-      begin
-        list = list(requested_list_id)
-        sub = list.subscribe(
-            parsed_body['email'],
-            parsed_body['fingerprint'],
-            parsed_body['admin'],
-            parsed_body['delivery_enabled']
-          )
-        logger.debug "subcription: #{sub.inspect}"
-        if sub.valid?
-          logger.debug "Subscribed: #{sub.inspect}"
-          redirect to("/subscriptions/#{sub.id}.json"), 201
-        else
-          client_error(sub, 422)
-        end
-      rescue ActiveRecord::RecordNotUnique
-        logger.error "Already subscribed"
-        status 422
-        json errors: {email: ['is already subscribed']}
-      end
-    end
-
-    get '/configurable_attributes.json' do
-      json(Subscription.configurable_attributes) + "\n"
-    end
-
-    get '/new.json' do
-      json Subscription.new
-    end
-
-    get '/:id.json' do |id|
-      json subscription(id)
-    end
-
-    put '/:id.json' do |id|
-      sub = subscription(id)
-      if sub.update(parsed_body)
-        200
-      else
-        client_error(sub, 422)
-      end
-    end
-
-    patch '/:id.json' do |id|
-      sub = subscription(id)
-      if sub.update(parsed_body)
-        200
-      else
-        client_error(sub)
-      end
-    end
-
-    delete '/:id.json' do |id|
-      if sub = subscription(id).destroy
-        200
-      else
-        client_error(sub)
-      end
-    end
-  end
-
-  namespace '/keys' do
-    get '.json' do
-      keys = list.keys.sort_by(&:email).map do |key|
-        key_to_json(key)
-      end
-      json keys
-    end
-
-    post '.json' do
-      input = parsed_body['keymaterial']
-      if ! input.match('BEGIN PGP')
-        input = Base64.decode64(input)
-      end
-      json list(requested_list_id).import_key(input)
-    end
-
-    get '/check_keys.json' do
-      json result: list.check_keys
-    end
-
-    get '/:fingerprint.json' do |fingerprint|
-      if key = list.key(fingerprint)
-        json key_to_json(key)
-      else
-        404
-      end
-    end
-
-    delete '/:fingerprint.json' do |fingerprint|
-      if list.delete_key(fingerprint)
-        200
-      else
-        404
-      end
-    end
-  end
-
-  def self.run!
-    super do |server|
-      server.ssl = true
-      server.ssl_options = {
-        :cert_chain_file  => Conf.api['tls_cert_file'],
-        :private_key_file => Conf.api['tls_key_file']
-      }
-    end
-  end
-
-  # Run this class as application
-  run!
-end
+require_relative '../lib/schleuder-api-daemon'
+SchleuderApiDaemon.run!
\ No newline at end of file
diff --git a/db/migrate/20140501103532_create_lists.rb b/db/migrate/20140501103532_create_lists.rb
index a3329d7..c091ddb 100644
--- a/db/migrate/20140501103532_create_lists.rb
+++ b/db/migrate/20140501103532_create_lists.rb
@@ -1,33 +1,39 @@
 class CreateLists < ActiveRecord::Migration
-  def change
-    create_table :lists do |t|
-      t.timestamps
-      t.string  :email
-      t.string  :fingerprint
-      t.string  :gpg_passphrase
-      t.string  :log_level, default: 'warn'
-      t.string  :default_mime, default: 'mime'
-      t.string  :subject_prefix, default: ''
-      t.string  :subject_prefix_in, default: ''
-      t.string  :subject_prefix_out, default: ''
-      t.string  :openpgp_header_preference, default: 'signencrypt'
-      t.text    :public_footer, default: ''
-      t.text    :headers_to_meta, default: '["from","to","date",":cc"]'
-      t.text    :bounces_drop_on_headers, default: '{"x-spam-flag":"yes"}'
-      t.text    :keywords_admin_only, default: '["unsubscribe", "unsubscribe", "delete-key"]'
-      t.text    :keywords_admin_notify, default: '["add-key"]'
-      t.boolean :send_encrypted_only, default: false
-      t.boolean :receive_encrypted_only, default: false
-      t.boolean :receive_signed_only, default: false
-      t.boolean :receive_authenticated_only, default: false
-      t.boolean :receive_from_subscribed_emailaddresses_only, default: false
-      t.boolean :receive_admin_only, default: false
-      t.boolean :keep_msgid, default: true
-      t.boolean :bounces_drop_all, default: false
-      t.boolean :bounces_notify_admins, default: true
-      t.boolean :include_list_headers, default: true
-      t.boolean :include_openpgp_header, default: true
-      t.integer :max_message_size_kb, default: 10240
+  def up
+    if ! table_exists?(:lists)
+      create_table :lists do |t|
+        t.timestamps
+        t.string  :email
+        t.string  :fingerprint
+        t.string  :gpg_passphrase
+        t.string  :log_level, default: 'warn'
+        t.string  :default_mime, default: 'mime'
+        t.string  :subject_prefix, default: ''
+        t.string  :subject_prefix_in, default: ''
+        t.string  :subject_prefix_out, default: ''
+        t.string  :openpgp_header_preference, default: 'signencrypt'
+        t.text    :public_footer, default: ''
+        t.text    :headers_to_meta, default: '["from","to","date",":cc"]'
+        t.text    :bounces_drop_on_headers, default: '{"x-spam-flag":"yes"}'
+        t.text    :keywords_admin_only, default: '["unsubscribe", "unsubscribe", "delete-key"]'
+        t.text    :keywords_admin_notify, default: '["add-key"]'
+        t.boolean :send_encrypted_only, default: false
+        t.boolean :receive_encrypted_only, default: false
+        t.boolean :receive_signed_only, default: false
+        t.boolean :receive_authenticated_only, default: false
+        t.boolean :receive_from_subscribed_emailaddresses_only, default: false
+        t.boolean :receive_admin_only, default: false
+        t.boolean :keep_msgid, default: true
+        t.boolean :bounces_drop_all, default: false
+        t.boolean :bounces_notify_admins, default: true
+        t.boolean :include_list_headers, default: true
+        t.boolean :include_openpgp_header, default: true
+        t.integer :max_message_size_kb, default: 10240
+      end
+    end
+
+    def down
+      drop_table(:lists)
     end
   end
 end
diff --git a/db/migrate/20140501112859_create_subscriptions.rb b/db/migrate/20140501112859_create_subscriptions.rb
index dc4029f..890dddb 100644
--- a/db/migrate/20140501112859_create_subscriptions.rb
+++ b/db/migrate/20140501112859_create_subscriptions.rb
@@ -1,15 +1,21 @@
 class CreateSubscriptions < ActiveRecord::Migration
-  def change
-    create_table :subscriptions do |t|
-      t.integer :list_id
-      t.string  :email
-      t.string  :fingerprint
-      t.boolean :admin, default: false
-      t.boolean :delivery_disabled, default: false
-      t.timestamps
+  def up
+    if ! table_exists?(:subscriptions)
+      create_table :subscriptions do |t|
+        t.integer :list_id
+        t.string  :email
+        t.string  :fingerprint
+        t.boolean :admin, default: false
+        t.boolean :delivery_disabled, default: false
+        t.timestamps
+      end
+
+      add_index :subscriptions, :list_id
+      add_index :subscriptions, [:email, :list_id], unique: true
     end
+  end
 
-    add_index :subscriptions, :list_id
-    add_index :subscriptions, [:email, :list_id], unique: true
+  def down
+    drop_table(:subscriptions)
   end
 end
diff --git a/db/migrate/201508092100_add_language_to_lists.rb b/db/migrate/201508092100_add_language_to_lists.rb
index a7f5df6..972a090 100644
--- a/db/migrate/201508092100_add_language_to_lists.rb
+++ b/db/migrate/201508092100_add_language_to_lists.rb
@@ -1,5 +1,11 @@
 class AddLanguageToLists < ActiveRecord::Migration
-  def change
-    add_column :lists, :language, :string, default: 'en'
+  def up
+    if ! column_exists?(:lists, :language)
+      add_column :lists, :language, :string, default: 'en'
+    end
+  end
+
+  def down
+    remove_column(:lists, :language)
   end
 end
diff --git a/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb b/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb
index 48171c6..ba89dae 100644
--- a/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb
+++ b/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb
@@ -1,5 +1,11 @@
 class AddForwardAllIncomingToAdmins < ActiveRecord::Migration
-  def change
-    add_column :lists, :forward_all_incoming_to_admins, :boolean, default: false
+  def up
+    if ! column_exists?(:lists, :forward_all_incoming_to_admins)
+      add_column :lists, :forward_all_incoming_to_admins, :boolean, default: false
+    end
+  end
+
+  def down
+    remove_column(:lists, :forward_all_incoming_to_admins)
   end
 end
diff --git a/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb b/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb
index 66b6bf8..af264ae 100644
--- a/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb
+++ b/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb
@@ -1,5 +1,11 @@
 class AddLogfilesToKeepToLists < ActiveRecord::Migration
-  def change
-    add_column :lists, :logfiles_to_keep, :integer, default: 2
+  def up
+    if ! column_exists?(:lists, :logfiles_to_keep)
+      add_column :lists, :logfiles_to_keep, :integer, default: 2
+    end
+  end
+
+  def down
+    remove_column(:lists, :logfiles_to_keep)
   end
 end
diff --git a/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb b/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb
index 132ac14..18b1bf7 100644
--- a/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb
+++ b/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb
@@ -1,7 +1,9 @@
 class RenameDeliveryDisabledToDeliveryEnabledAndChangeDefault < ActiveRecord::Migration
   def up
-    rename_column :subscriptions, :delivery_disabled, :delivery_enabled
-    change_column_default :subscriptions, :delivery_enabled, true
+    if column_exists?(:subscriptions, :delivery_disabled)
+      rename_column :subscriptions, :delivery_disabled, :delivery_enabled
+      change_column_default :subscriptions, :delivery_enabled, true
+    end
   end
 
   def down
diff --git a/db/migrate/201508261815_strip_gpg_passphrase.rb b/db/migrate/201508261815_strip_gpg_passphrase.rb
index 891905d..7fde0fb 100644
--- a/db/migrate/201508261815_strip_gpg_passphrase.rb
+++ b/db/migrate/201508261815_strip_gpg_passphrase.rb
@@ -1,6 +1,8 @@
 class StripGpgPassphrase < ActiveRecord::Migration
   def up
-    remove_column :lists, :gpg_passphrase
+    if column_exists?(:lists, :gpg_passphrase)
+      remove_column :lists, :gpg_passphrase
+    end
   end
 
   def down
diff --git a/db/migrate/20170713215059_add_internal_footer_to_list.rb b/db/migrate/20170713215059_add_internal_footer_to_list.rb
new file mode 100644
index 0000000..5321fdf
--- /dev/null
+++ b/db/migrate/20170713215059_add_internal_footer_to_list.rb
@@ -0,0 +1,11 @@
+class AddInternalFooterToList < ActiveRecord::Migration
+  def up
+    if ! column_exists?(:lists, :internal_footer)
+      add_column :lists, :internal_footer, :text, default: ''
+    end
+  end
+
+  def down
+    remove_column(:lists, :internal_footer)
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c084225..6ab8e9a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20160501172700) do
+ActiveRecord::Schema.define(version: 20170713215059) do
 
   create_table "lists", force: :cascade do |t|
     t.datetime "created_at"
@@ -43,6 +43,7 @@ ActiveRecord::Schema.define(version: 20160501172700) do
     t.string   "language",                                    limit: 255, default: "en"
     t.boolean  "forward_all_incoming_to_admins",                          default: false
     t.integer  "logfiles_to_keep",                                        default: 2
+    t.text     "internal_footer",                                         default: ""
   end
 
   create_table "subscriptions", force: :cascade do |t|
diff --git a/etc/list-defaults.yml b/etc/list-defaults.yml
index 3a413db..f830f94 100644
--- a/etc/list-defaults.yml
+++ b/etc/list-defaults.yml
@@ -60,7 +60,12 @@ keywords_admin_only:
 keywords_admin_notify: 
 - add-key
 
-# Public footer to append each email that is sent to non-subscribed addresses.
+# Footer to append to each email that is sent to a subscribed address. Will not
+# be included in messages to non-subscribed addresses.
+internal_footer:
+
+# Footer to append to each email that is sent to non-subscribed addresses. Will
+# not be included in messages to subscribed addresses.
 public_footer:
 
 # Prefix to be inserted into the subject of every email that is validly signed
diff --git a/etc/schleuder.yml b/etc/schleuder.yml
index 116e7ac..f95cb54 100644
--- a/etc/schleuder.yml
+++ b/etc/schleuder.yml
@@ -16,6 +16,9 @@ log_level: warn
 #keyserver: hkps://hkps.pool.sks-keyservers.net
 # If you have gnupg 2.1 and TOR running locally, use a onion-keyserver:
 #keyserver: hkp://jirk5u4osbsr34t5.onion
+# If you have an OS-wide defined keyserver, specify a blank value to have that
+# one used:
+#keyserver: 
 # The default works for all supported versions of gnupg:
 keyserver: pool.sks-keyservers.net
 
diff --git a/bin/schleuder-api-daemon b/lib/schleuder-api-daemon.rb
similarity index 75%
copy from bin/schleuder-api-daemon
copy to lib/schleuder-api-daemon.rb
index 80eaeaf..4999d49 100755
--- a/bin/schleuder-api-daemon
+++ b/lib/schleuder-api-daemon.rb
@@ -20,9 +20,6 @@ end
 
 class SchleuderApiDaemon < Sinatra::Base
   register Sinatra::Namespace
-  use Rack::Auth::Basic, "Schleuder API Daemon" do |username, key|
-    username == 'schleuder' && Conf.api_valid_api_keys.include?(key)
-  end
 
   configure do
     set :server, :thin
@@ -36,6 +33,7 @@ class SchleuderApiDaemon < Sinatra::Base
   end
 
   before do
+    authenticate!
     cast_param_values
   end
 
@@ -68,6 +66,25 @@ class SchleuderApiDaemon < Sinatra::Base
   end
 
   helpers do
+    def valid_credentials?
+      @auth ||=  Rack::Auth::Basic::Request.new(request.env)
+      if @auth.provided? && @auth.basic? && @auth.credentials.present?
+        username, api_key = @auth.credentials
+        username == 'schleuder' && Conf.api_valid_api_keys.include?(api_key)
+      else
+        false
+      end
+    end
+
+    def authenticate!
+      # Be careful to use path_info() — it can be changed by other filters!
+      return if request.path_info == '/status.json'
+      if ! valid_credentials?
+        headers['WWW-Authenticate'] = 'Basic realm="Schleuder API Daemon"'
+        halt 401, "Not authorized\n"
+      end
+    end
+
     def list(id_or_email=nil)
       if id_or_email.blank?
         if params[:list_id].present?
@@ -146,14 +163,45 @@ class SchleuderApiDaemon < Sinatra::Base
       end
     end
 
-    def key_to_json(key)
-      {
+    def key_to_hash(key, include_keydata=false)
+      hash = {
         fingerprint: key.fingerprint,
         email: key.email,
-        ascii: key.armored,
         expiry: key.expires,
+        generated_at: key.generated_at,
+        primary_uid: key.primary_uid.uid,
+        oneline: key.oneline,
         trust_issues: key.usability_issue
       }
+      if include_keydata
+        hash[:description] = key.to_s
+        hash[:ascii] = key.armored
+      end
+      hash
+    end
+
+    def set_x_messages(messages)
+      if messages.present?
+        headers 'X-Messages' => Array(messages).join(' // ').gsub(/\n/, ' // ')
+      end
+    end
+
+    def find_key_material
+      key_material = parsed_body['key_material'].presence
+      # By convention key_material is either ASCII or base64-encoded.
+      if key_material && ! key_material.match('BEGIN PGP')
+        key_material = Base64.decode64(key_material)
+      end
+      key_material
+    end
+
+    def find_attributes_from_body(attribs)
+      Array(attribs).inject({}) do |memo, attrib|
+        if parsed_body.has_key?(attrib)
+          memo[attrib] = parsed_body[attrib]
+        end
+        memo
+      end
     end
   end
 
@@ -166,16 +214,15 @@ class SchleuderApiDaemon < Sinatra::Base
       listname = parsed_body['email']
       fingerprint = parsed_body['fingerprint']
       adminaddress = parsed_body['adminaddress']
+      adminfingerprint = parsed_body['adminfingerprint']
       adminkey = parsed_body['adminkey']
-      list, messages = ListBuilder.new({email: listname, fingerprint: fingerprint}, adminaddress, adminkey).run
+      list, messages = ListBuilder.new({email: listname, fingerprint: fingerprint}, adminaddress, adminfingerprint, adminkey).run
       if list.nil?
         client_error(messages, 422)
       elsif ! list.valid?
         client_error(list, 422)
       else
-        if messages.present?
-          headers 'X-Messages' => messages.join(' // ').gsub(/\n/, ' // ')
-        end
+        set_x_messages(messages)
         body json(list)
       end
     end
@@ -248,15 +295,19 @@ class SchleuderApiDaemon < Sinatra::Base
     post '.json' do
       begin
         list = list(requested_list_id)
-        sub = list.subscribe(
+        # We don't have to care about nil-values, subscribe() does that for us.
+        sub, msgs = list.subscribe(
             parsed_body['email'],
             parsed_body['fingerprint'],
             parsed_body['admin'],
-            parsed_body['delivery_enabled']
+            parsed_body['delivery_enabled'],
+            find_key_material
           )
+        set_x_messages(msgs)
         logger.debug "subcription: #{sub.inspect}"
         if sub.valid?
           logger.debug "Subscribed: #{sub.inspect}"
+          # TODO: why redirect instead of respond with result?
           redirect to("/subscriptions/#{sub.id}.json"), 201
         else
           client_error(sub, 422)
@@ -282,7 +333,16 @@ class SchleuderApiDaemon < Sinatra::Base
 
     put '/:id.json' do |id|
       sub = subscription(id)
-      if sub.update(parsed_body)
+      list = sub.list
+      args = find_attributes_from_body(%w[email fingerprint admin delivery_enabled])
+      fingerprint, messages = list.import_key_and_find_fingerprint(find_key_material)
+      set_x_messages(messages)
+      # For an already existing subscription, only update fingerprint if a
+      # new one has been selected from the upload.
+      if fingerprint.present?
+        args["fingerprint"] = fingerprint
+      end
+      if sub.update(args)
         200
       else
         client_error(sub, 422)
@@ -310,7 +370,7 @@ class SchleuderApiDaemon < Sinatra::Base
   namespace '/keys' do
     get '.json' do
       keys = list.keys.sort_by(&:email).map do |key|
-        key_to_json(key)
+        key_to_hash(key)
       end
       json keys
     end
@@ -329,7 +389,7 @@ class SchleuderApiDaemon < Sinatra::Base
 
     get '/:fingerprint.json' do |fingerprint|
       if key = list.key(fingerprint)
-        json key_to_json(key)
+        json key_to_hash(key, true)
       else
         404
       end
@@ -353,7 +413,4 @@ class SchleuderApiDaemon < Sinatra::Base
       }
     end
   end
-
-  # Run this class as application
-  run!
 end
diff --git a/lib/schleuder/conf.rb b/lib/schleuder/conf.rb
index 6eff819..7744953 100644
--- a/lib/schleuder/conf.rb
+++ b/lib/schleuder/conf.rb
@@ -1,3 +1,5 @@
+require 'erb'
+
 module Schleuder
   class Conf
     include Singleton
@@ -120,7 +122,7 @@ module Schleuder
     def load_config_file(filename)
       file = Pathname.new(filename)
       if file.readable?
-        YAML.load(file.read)
+        YAML.load(ERB.new(file.read).result)
       else
         {}
       end
diff --git a/lib/schleuder/gpgme/ctx.rb b/lib/schleuder/gpgme/ctx.rb
index 429d619..8333efa 100644
--- a/lib/schleuder/gpgme/ctx.rb
+++ b/lib/schleuder/gpgme/ctx.rb
@@ -14,11 +14,39 @@ module GPGME
       result
     end
 
+    # TODO: find solution for I18n — could be a different language in API-clients than here!
+    def interpret_import_result(import_result)
+      case import_result.imports.size
+      when 1
+        import_status = import_result.imports.first
+        if import_status.action == 'not imported'
+          [nil, "Key #{import_status.fpr} could not be imported!"]
+        else
+          [import_status.fpr, nil]
+        end
+      when 0
+        [nil, "The given key material did not contain any keys!"]
+      else
+        # TODO: report import-stati of the keys?
+        [nil, "The given key material contained more than one key, could not determine which fingerprint to use. Please set it manually!"]
+      end
+    end
+
     def find_keys(input=nil, secret_only=nil)
       _, input = clean_and_classify_input(input)
       keys(input, secret_only)
     end
 
+    def find_distinct_key(input=nil, secret_only=nil)
+      _, input = clean_and_classify_input(input)
+      keys = keys(input, secret_only)
+      if keys.size == 1
+        keys.first
+      else
+        nil
+      end
+    end
+
     def clean_and_classify_input(input)
       case input
       when /.*?([^ <>]+@[^ <>]+).*?/
@@ -70,7 +98,7 @@ module GPGME
     end
 
     def refresh_key(fingerprint)
-      args = "--keyserver #{Conf.keyserver} --refresh-keys #{fingerprint}"
+      args = "#{keyserver_arg} --refresh-keys #{fingerprint}"
       gpgerr, gpgout, exitcode = self.class.gpgcli(args)
 
       if exitcode > 0
@@ -84,7 +112,7 @@ module GPGME
       else
         lines = translate_output('key_updated', gpgout).reject do |line|
           # Reduce the noise a little.
-          line.match(/.* \(unchanged\)\.$/)
+          line.match(/.* \(unchanged\):$/)
         end
         lines.join("\n")
       end
@@ -107,13 +135,13 @@ module GPGME
     def fetch_key_gpg_arguments_for(input)
       case input
       when Conf::FINGERPRINT_REGEXP
-        "--keyserver #{Conf.keyserver} --recv-key #{input}"
+        "#{keyserver_arg} --recv-key #{input}"
       when /^http/
         "--fetch-key #{input}"
       when /@/
         # --recv-key doesn't work with email-addresses, so we use --locate-key
         # restricted to keyservers.
-        "--keyserver #{Conf.keyserver} --auto-key-locate keyserver --locate-key #{input}"
+        "#{keyserver_arg} --auto-key-locate keyserver --locate-key #{input}"
       else
         [nil, I18n.t("fetch_key.invalid_input")]
       end
@@ -122,8 +150,9 @@ module GPGME
     def translate_output(locale_key, gpgoutput)
       import_states = translate_import_data(gpgoutput)
       strings = import_states.map do |fingerprint, states|
-        I18n.t(locale_key, { fingerprint: fingerprint,
-                             states: states.join(', ') })
+        key = find_distinct_key(fingerprint)
+        I18n.t(locale_key, { key_oneline: key.oneline,
+                             states: states.to_sentence })
       end
       strings
     end
@@ -219,5 +248,13 @@ module GPGME
         File.delete(path)
       end
     end
+
+    def keyserver_arg
+      if Conf.keyserver.present?
+        "--keyserver #{Conf.keyserver}"
+      else
+        ""
+      end
+    end
   end
 end
diff --git a/lib/schleuder/gpgme/key.rb b/lib/schleuder/gpgme/key.rb
index a4bca4f..f6bd06d 100644
--- a/lib/schleuder/gpgme/key.rb
+++ b/lib/schleuder/gpgme/key.rb
@@ -18,6 +18,41 @@ module GPGME
       s
     end
 
+    def generated_at
+      primary_subkey.timestamp
+    end
+
+    def expired?
+      expired.present?
+    end
+
+    def oneline
+      @oneline ||= 
+        begin
+          datefmt = '%Y-%m-%d'
+          attribs = [
+            "0x#{fingerprint}",
+            email,
+            generated_at.strftime(datefmt)
+          ]
+          if usability_issue.present?
+            case usability_issue
+            when :expired
+              attribs << "[expired: #{expires.strftime(datefmt)}]"
+            when :revoked
+              # TODO: add revocation date when it's available.
+              attribs << "[revoked]"
+            else
+              attribs << "[#{usability_issue}]"
+            end
+          end
+          if expires? && ! expired?
+            attribs << "[expires: #{expires.strftime(datefmt)}]"
+          end
+          attribs.join(' ')
+        end
+    end
+
     def armored
       "#{self.to_s}\n\n#{export(armor: true).read}"
     end
diff --git a/lib/schleuder/list.rb b/lib/schleuder/list.rb
index c15aba3..d981ba7 100644
--- a/lib/schleuder/list.rb
+++ b/lib/schleuder/list.rb
@@ -60,7 +60,7 @@ module Schleuder
                 # TODO: find out why we break translations and available_locales if we use I18n.available_locales here.
                 in: %w(de en),
               }
-    validates :public_footer,
+    validates :public_footer, :internal_footer,
               allow_blank: true,
               format: {
                 with: /\A[[:graph:]\s]*\z/i,
@@ -107,6 +107,8 @@ module Schleuder
       gpg.find_keys(identifier, secret_only)
     end
 
+    # TODO: find better name for this method. It does more than the current
+    # name suggests (filtering for capability).
     def distinct_key(identifier)
       keys = keys(identifier).select { |key| key.usable_for?(:encrypt) }
       if keys.size == 1
@@ -120,6 +122,13 @@ module Schleuder
       gpg.keyimport(importable)
     end
 
+    def import_key_and_find_fingerprint(key_material)
+      return nil if key_material.blank?
+      
+      import_result = import_key(key_material)
+      gpg.interpret_import_result(import_result)
+    end
+
     def delete_key(fingerprint)
       if key = keys(fingerprint).first
         key.delete!
@@ -160,8 +169,7 @@ module Schleuder
       expiring.each do |key,days|
         text << I18n.t('key_expires', {
                           days: days,
-                          fingerprint: key.fingerprint,
-                          email: key.email
+                          key_oneline: key.oneline
                       })
         text << "\n"
       end
@@ -169,8 +177,7 @@ module Schleuder
       unusable.each do |key,usability_issue|
         text << I18n.t('key_unusable', {
                           usability_issue: usability_issue,
-                          fingerprint: key.fingerprint,
-                          email: key.email
+                          key_oneline: key.oneline
                       })
         text << "\n"
       end
@@ -261,21 +268,29 @@ module Schleuder
       @listdir ||= self.class.listdir(self.email)
     end
 
-    # TODO: get rid of this method in the future
-    def subscribe(email, fingerprint=nil, adminflag=false, deliveryflag=true)
-      # Ensure we have true or false as values for these two attributes.
-      admin            =  (['true', '1'].include?  adminflag.to_s)
-      delivery_enabled = !(['false', '0'].include? deliveryflag.to_s)
-
-      sub = Subscription.new(
+    # A convenience-method to simplify other code.
+    def subscribe(email, fingerprint=nil, adminflag=nil, deliveryflag=nil, key_material=nil)
+      messages = nil
+      args = {
           list_id: self.id,
-          email: email,
-          fingerprint: fingerprint,
-          admin: admin,
-          delivery_enabled: delivery_enabled
-        )
-      sub.save
-      sub
+          email: email
+      }
+      if key_material.present?
+        fingerprint, messages = import_key_and_find_fingerprint(key_material)
+      end
+      args[:fingerprint] = fingerprint
+      # ActiveRecord does not treat nil as falsy for boolean columns, so we
+      # have to avoid that in order to not receive an invalid object. The
+      # database will use the column's default-value if no value is being
+      # given. (I'd rather not duplicate the defaults here.)
+      if ! adminflag.nil?
+        args[:admin] = adminflag
+      end
+      if ! deliveryflag.nil?
+        args[:delivery_enabled] = deliveryflag
+      end
+      subscription = Subscription.create(args)
+      [subscription, messages]
     end
 
     def unsubscribe(email, delete_key=false)
@@ -328,6 +343,7 @@ module Schleuder
 
     def send_to_subscriptions(mail)
       logger.debug "Sending to subscriptions."
+      mail.add_internal_footer!
       self.subscriptions.each do |subscription|
         begin
           subscription.send_mail(mail)
@@ -335,6 +351,7 @@ module Schleuder
           msg = I18n.t('errors.delivery_error',
                        { email: subscription.email, error: exc.to_s })
           logger.error msg
+          logger.error exc
         end
       end
     end
diff --git a/lib/schleuder/list_builder.rb b/lib/schleuder/list_builder.rb
index 70d814d..de7e3a0 100644
--- a/lib/schleuder/list_builder.rb
+++ b/lib/schleuder/list_builder.rb
@@ -1,10 +1,11 @@
 module Schleuder
   class ListBuilder
-    def initialize(list_attributes, adminemail=nil, adminkey=nil)
+    def initialize(list_attributes, adminemail=nil, adminfingerprint=nil, adminkey=nil)
       @list_attributes = list_attributes.with_indifferent_access
       @listname = list_attributes[:email]
       @fingerprint = list_attributes[:fingerprint]
       @adminemail = adminemail
+      @adminfingerprint = adminfingerprint
       @adminkey = adminkey
     end
 
@@ -48,31 +49,16 @@ module Schleuder
 
       list.save!
 
-      if @adminkey.present?
-        import_result = list.import_key(@adminkey)
-        # Get the fingerprint of the imported key if it was exactly one. If it
-        # was imported or was already present doesn't matter.
-        if import_result.considered == 1
-          admin_fpr = import_result.imports.first.fpr
-        end
-      end
-
-      if @adminemail.present?
-        # Try if we can find the admin-key "manually". Maybe it's present
-        # in the keyring aleady.
-        if admin_fpr.blank?
-          key = list.distinct_key(@adminemail)
-          if key
-            admin_fpr = key.fingerprint
-          end
-        end
-        sub = list.subscribe(@adminemail, admin_fpr, true)
+      if @adminemail.blank?
+        msg = nil
+      else
+        sub, msg = list.subscribe(@adminemail, @adminfingerprint, true, true, @adminkey)
         if sub.errors.present?
-          raise ActiveModelError.new(sub.errors)
+          raise Errors::ActiveModelError.new(sub.errors)
         end
       end
 
-      list
+      [list, msg]
     end
 
     def gpg
diff --git a/lib/schleuder/mail/message.rb b/lib/schleuder/mail/message.rb
index 778ec69..c7569a8 100644
--- a/lib/schleuder/mail/message.rb
+++ b/lib/schleuder/mail/message.rb
@@ -88,6 +88,7 @@ module Mail
         # We copied the content-headers, so we need to copy the body encoded.
         # Otherwise the content might become unlegible.
         wrapper_part.body = self.body.encoded
+        wrapper_part.charset = self.body.charset || self.class.default_charset
       end
       clean.add_part(wrapper_part)
 
@@ -99,17 +100,13 @@ module Mail
       self.parts.unshift(parts.delete_at(parts.size-1))
     end
 
-    def add_footer!
+    def add_public_footer!
       # Add public_footer unless it's empty?.
-      if self.list.present? && ! self.list.public_footer.to_s.strip.empty?
-        footer_part = Mail::Part.new
-        footer_part.body = list.public_footer.strip
-        if parts.size == 1 && parts.first.mime_type == 'multipart/mixed' && parts.first.parts.size == 1 && parts.first.parts.first.mime_type == 'text/plain'
-          self.parts.first.add_part footer_part
-        else
-          self.add_part footer_part
-        end
-      end
+      add_footer!(:public_footer)
+    end
+
+    def add_internal_footer!
+      add_footer!(:internal_footer)
     end
 
     def was_encrypted?
@@ -207,7 +204,7 @@ module Mail
       end
 
       @keywords = []
-      part.body = part.decoded.lines.map.with_index do |line, i|
+      lines = part.decoded.lines.map.with_index do |line, i|
         # Break after some lines to not run all the way through maybe huge emails.
         if i > 1000
           break
@@ -222,7 +219,14 @@ module Mail
         else
           line
         end
-      end.compact.join
+      end
+
+      # Work around problems with re-encoding the body. If we delete the
+      # content-transfer-encoding prior to re-assigning the body, and let Mail
+      # decide itself how to encode, it works. If we don't, some
+      # character-sequences are not properly re-encoded.
+      part.content_transfer_encoding = nil
+      part.body = lines.compact.join
 
       @keywords
     end
@@ -408,6 +412,26 @@ module Mail
     private
 
 
+    def add_footer!(footer_attribute)
+      if self.list.blank? || self.list.send(footer_attribute).to_s.empty?
+        return
+      end
+      footer_part = Mail::Part.new
+      footer_part.body = self.list.send(footer_attribute).to_s
+      if wrapped_single_text_part?
+        self.parts.first.add_part footer_part
+      else
+        self.add_part footer_part
+      end
+    end
+
+    def wrapped_single_text_part?
+      parts.size == 1 && 
+        parts.first.mime_type == 'multipart/mixed' && 
+        parts.first.parts.size == 1 && 
+        parts.first.parts.first.mime_type == 'text/plain'
+    end
+
     def _add_subject_prefix(suffix)
       attrib = "subject_prefix"
       if suffix
diff --git a/lib/schleuder/plugin_runners/base.rb b/lib/schleuder/plugin_runners/base.rb
index 9448d05..57d55f5 100644
--- a/lib/schleuder/plugin_runners/base.rb
+++ b/lib/schleuder/plugin_runners/base.rb
@@ -25,7 +25,7 @@ module Schleuder
         return error if error
 
         command = keyword.gsub('-', '_')
-        if command == 'listname'
+        if ['list_name', 'listname'].include? (command)
           return nil
         elsif ! @plugin_module.respond_to?(command)
           return I18n.t('plugins.unknown_keyword', keyword: keyword)
@@ -66,7 +66,7 @@ module Schleuder
       def check_listname_keyword
         return nil if @mail.keywords.blank?
 
-        listname_kw = @mail.keywords.assoc('listname')
+        listname_kw = @mail.keywords.assoc('list-name') || @mail.keywords.assoc('listname')
         if listname_kw.blank?
           @mail.reply_to_signer I18n.t(:missing_listname_keyword_error)
           exit
diff --git a/lib/schleuder/plugins/key_management.rb b/lib/schleuder/plugins/key_management.rb
index 8cfdf74..4826547 100644
--- a/lib/schleuder/plugins/key_management.rb
+++ b/lib/schleuder/plugins/key_management.rb
@@ -40,7 +40,7 @@ module Schleuder
     end
 
     def self.list_keys(arguments, list, mail)
-      args = arguments.presence
+      args = Array(arguments.presence || '')
       args.map do |argument|
         # In this case it shall be allowed to match keys by arbitrary
         # sub-strings, therefore we use `list.gpg` directly to not have the
@@ -76,6 +76,9 @@ module Schleuder
       end
     end
 
+    # helper methods
+    private
+
     def self.is_armored_key?(material)
       return false unless /^-----BEGIN PGP PUBLIC KEY BLOCK-----$/ =~ material
       return false unless /^-----END PGP PUBLIC KEY BLOCK-----$/ =~ material
diff --git a/lib/schleuder/plugins/resend.rb b/lib/schleuder/plugins/resend.rb
index 5422ccd..9c439c4 100644
--- a/lib/schleuder/plugins/resend.rb
+++ b/lib/schleuder/plugins/resend.rb
@@ -24,7 +24,34 @@ module Schleuder
       resend_it_cc(arguments, mail, true)
     end
 
+    def self.resend_unencrypted(arguments, list, mail)
+      do_resend_unencrypted(arguments, list, mail, :to)
+    end
+
+    def self.resend_cc_unencrypted(arguments, list, mail)
+      do_resend_unencrypted(arguments, list, mail, :cc)
+    end
+
+    # helper methods
+    private
+
+    def self.do_resend_unencrypted(arguments, list, mail, target)
+      if ! resend_recipients_valid?(mail, arguments)
+        return false
+      end
+
+      recip_map = Hash[Array(arguments).map{|email| [email,''] }]
+
+      if do_resend(mail, recip_map, target, false)
+        mail.add_subject_prefix_out!
+      end
+    end
+
     def self.resend_it_cc(arguments, mail, encrypted_only)
+      if ! resend_recipients_valid?(mail, arguments)
+        return false
+      end
+
       recip_map = map_with_keys(mail, arguments, encrypted_only)
 
       # Only continue if all recipients are still here.
@@ -38,6 +65,10 @@ module Schleuder
     end
 
     def self.resend_it(arguments, mail, encrypted_only)
+      if ! resend_recipients_valid?(mail, arguments)
+        return false
+      end
+
       recip_map = map_with_keys(mail, arguments, encrypted_only)
 
       resent_stati = recip_map.map do |email, key|
@@ -63,7 +94,7 @@ module Schleuder
       # Compose and send email
       new = mail.clean_copy
       new[to_or_cc] = recipients_map.keys
-      new.add_footer!
+      new.add_public_footer!
       new.sender = mail.list.bounce_address
       # `dup` gpg_opts because `deliver` changes their value and we need them
       # below to determine encryption!
@@ -150,5 +181,16 @@ module Schleuder
         'resent_cc'
       end
     end
+
+    def self.resend_recipients_valid?(mail, recipients)
+      all_valid = true
+      Array(recipients).each do |address|
+        if ! address.match(Conf::EMAIL_REGEXP)
+          mail.add_pseudoheader(:error, I18n.t("plugins.resend.invalid_recipient", address: address))
+          all_valid = false
+        end
+      end
+      all_valid
+    end
   end
 end
diff --git a/lib/schleuder/plugins/sign_this.rb b/lib/schleuder/plugins/sign_this.rb
index 4a7062d..86a7841 100644
--- a/lib/schleuder/plugins/sign_this.rb
+++ b/lib/schleuder/plugins/sign_this.rb
@@ -14,6 +14,9 @@ module Schleuder
       end
     end
 
+    # helper methods
+    private
+
     def self.make_signature_part(attachment, list)
       material = attachment.body.to_s
       return nil if material.strip.blank?
diff --git a/lib/schleuder/plugins/subscription_management.rb b/lib/schleuder/plugins/subscription_management.rb
index d95ec02..541b5c9 100644
--- a/lib/schleuder/plugins/subscription_management.rb
+++ b/lib/schleuder/plugins/subscription_management.rb
@@ -3,17 +3,18 @@ module Schleuder
     def self.subscribe(arguments, list, mail)
       email = arguments.shift
 
-      # Pop the last two in order to be tolerant about spaces in the
-      # fingerprint.
-      deliveryflag = arguments.pop
-      adminflag = arguments.pop
-      # Use the remainders as fingerprint. This enables tolerating spaces.
-      fingerprint = arguments.join
-      if fingerprint.present?
-        fingerprint.sub!(/^0x/, '')
+      if arguments.present?
+        # Collect all arguments that look like fingerprint-material
+        fingerprint = ''
+        while arguments.first.present? && arguments.first.match(/\A(0x)?[a-f0-9]+/i)
+          fingerprint << arguments.shift
+        end
+        # Use possibly remaining args as flags.
+        adminflag = arguments.shift
+        deliveryflag = arguments.shift
       end
 
-      sub = list.subscribe(email, fingerprint, adminflag, deliveryflag)
+      sub, _ = list.subscribe(email, fingerprint, adminflag, deliveryflag)
 
       if sub.persisted?
         I18n.t(
diff --git a/lib/schleuder/version.rb b/lib/schleuder/version.rb
index f7ea37f..23ec4b1 100644
--- a/lib/schleuder/version.rb
+++ b/lib/schleuder/version.rb
@@ -1,3 +1,3 @@
 module Schleuder
-  VERSION = '3.1.2'
+  VERSION = '3.2.0'
 end
diff --git a/locales/de.yml b/locales/de.yml
index 2d97958..390a44e 100644
--- a/locales/de.yml
+++ b/locales/de.yml
@@ -7,6 +7,8 @@ de:
         inclusion: "muss einem der folgenden Werte entsprechen: debug, info, warn, error"
       openpgp_header_preference:
         inclusion: "muss einem der folgenden Werte entsprechen: sign, encrypt, signencrypt, unprotected, none"
+      internal_footer:
+        invalid: "enthält nicht druckbare Zeichen"
       public_footer:
         invalid: "enthält nicht druckbare Zeichen"
     invalid_email: "ist keine valide E-Mail-Adresse"
@@ -95,6 +97,7 @@ de:
       not_resent_no_keys: Resending an <%{email}> fehlgeschlagen (%{num_keys} Schlüssel gefunden und unverschlüsseltes Senden verboten).
       encrypted_to: Verschlüsselt an
       unencrypted_to: Unverschlüsselt an
+      invalid_recipient: "Ungültige Emailadresse für resend: %{address}"
     subscription_management:
       forbidden: "Fehler: Du bist nicht berechtigt, das Abo für %{email} zu löschen."
       is_not_subscribed: Kein Abo für %{email} gefunden.
@@ -127,8 +130,8 @@ de:
   no_output_result: Deine Email ergab keinen Ausgabe-Text.
   owner_forward_prefix: Die folgende Email ging für die Listen-Besitzer/innen ein.
   no_keywords_error: Deine Email enthielt keine Schlüsselwörter, daher gab es nichts zu tun.
-  missing_listname_keyword_error: Deine Email enthielt nicht das notwendige X-LISTNAME-Schlüsselwort, daher wurde sie zurückgewiesen.
-  wrong_listname_keyword_error: Deine Email enthielt ein falsches X-LISTNAME-Schlüsselwort. Der Wert dieses Schlüsselworts muss der Emailadresse dieser Liste gleichen.
+  missing_listname_keyword_error: Deine Email enthielt nicht das notwendige X-LIST-NAME-Schlüsselwort, daher wurde sie zurückgewiesen.
+  wrong_listname_keyword_error: Deine Email enthielt ein falsches X-LIST-NAME-Schlüsselwort. Der Wert dieses Schlüsselworts muss der Emailadresse dieser Liste gleichen.
   bounces_drop_all: Die angehängte Email hätte zurückgewiesen (bounced) werden sollen, wurde aber stillschweigend fallen gelassen, weil die Konfiguration dieser Liste definiert, dass für diese Liste nie Email zurückgewiesen werden soll.
   bounces_drop_on_headers: "Die angehängte Email hätte zurückgewiesen (bounce) werden sollen, wurde aber stillschweigend fallen gelassen, weil diese Kopfzeile gefunden wurde: %{key}: %{value}"
   bounces_notify_admins: "Die angehängte Email wurde mit folgender Nachricht zurückgewiesen:"
@@ -139,15 +142,23 @@ de:
   automated_message_subject: Automatische Nachricht empfangen
   check_keys: Schlüsselprüfung
   check_keys_intro: "Bitte kümmere dich um die folgenden Schlüssel für Liste %{email}."
-  key_expires: Schlüssel %{fingerprint} %{email} läuft in %{days} Tagen ab.
-  key_unusable: Schlüssel %{fingerprint} %{email} ist %{usability_issue}.
+  key_expires: |
+    Dieser Schlüssel läuft in %{days} Tagen ab:
+    %{key_oneline}
+  key_unusable: |
+    Dieser Schlüssel ist %{usability_issue}:
+    %{key_oneline}
   missed_message_due_to_unusable_key: "Du hast eine Email von %{list_email} verpasst weil mit deinem Abo kein (benutzbarer) OpenPGP-Schlüssel verknüpft ist. Bitte kümmere dich darum."
   refresh_keys: Schlüsselaktualisierung
   refresh_keys_intro: "Die Aktualisierung aller Schlüssel des Schlüsselrings für Liste %{email} ergab dies:"
   pin_keys: Schlüsselpinning
   pin_keys_intro: "Die Überprüfung aller Abos der Liste %{email} ergab, dass wir für folgende Abos einen Schlüssel zur Verwendung festgelegt haben:"
-  key_updated: Schlüssel %{fingerprint} wurde aktualisiert (%{states}).
-  key_fetched: Schlüssel %{fingerprint} wurde geholt (%{states}).
+  key_updated: |
+    Dieser Schlüssel wurde aktualisiert (%{states}):
+    %{key_oneline}
+  key_fetched: |
+    Dieser Schlüssel wurde geholt (%{states}):
+    %{key_oneline}
   import_states:
     unchanged: unverändert
     new_key: neuer Schlüssel
diff --git a/locales/en.yml b/locales/en.yml
index bf3c7b4..d6bd874 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -7,6 +7,8 @@ en:
         inclusion: "must be one of: debug, info, warn, error"
       openpgp_header_preference:
         inclusion: "must be one of: sign, encrypt, signencrypt, unprotected, none"
+      internal_footer:
+        invalid: "includes non-printable characters"
       public_footer:
         invalid: "includes non-printable characters"
     invalid_email: "is not a valid email address"
@@ -95,6 +97,7 @@ en:
       not_resent_no_keys: Resending to <%{email}> failed (%{num_keys} keys found and unencrypted sending disallowed).
       encrypted_to: Encrypted to
       unencrypted_to: Unencrypted to
+      invalid_recipient: "Invalid email-address for resending: %{address}"
     subscription_management:
       forbidden: "Error: You're not allowed to unsubscribe %{email}."
       is_not_subscribed: "%{email} is not subscribed."
@@ -127,8 +130,8 @@ en:
   no_output_result: Your message resulted in no output.
   owner_forward_prefix: The following message was received for the list-owners.
   no_keywords_error: Your message didn't contain any keywords, thus there was nothing to do.
-  missing_listname_keyword_error: Your message didn't contain the mandatory X-LISTNAME-keyword, thus it was rejected.
-  wrong_listname_keyword_error: Your message contained a wrong X-LISTNAME-keyword. The value of that keyword must match the email address of this list.
+  missing_listname_keyword_error: Your message didn't contain the mandatory X-LIST-NAME-keyword, thus it was rejected.
+  wrong_listname_keyword_error: Your message contained a wrong X-LIST-NAME-keyword. The value of that keyword must match the email address of this list.
   bounces_drop_all: The attached message should have been bounced but was dropped without further notice because the list's configuration defines that no message should ever be bounced.
   bounces_drop_on_headers: "The attached message should have been bounced but was dropped without further notice because it matched this header-line: %{key}: %{value}"
   bounces_notify_admins: "The attached message was bounced with the following notice:"
@@ -139,15 +142,23 @@ en:
   automated_message_subject: Automated message received
   check_keys: Keys check
   check_keys_intro: "Please take care of these keys for list %{email}."
-  key_expires: Key %{fingerprint} expires in %{days} days.
-  key_unusable: Key %{fingerprint} is %{usability_issue}.
+  key_expires: |
+    This key expires in %{days} days:
+    %{key_oneline}
+  key_unusable: |
+    This key is %{usability_issue}:
+    %{key_oneline}
   missed_message_due_to_unusable_key: "You missed an email from %{list_email} because your subscription isn't associated with a (usable) OpenPGP key. Please fix this."
   refresh_keys: Keys update
   refresh_keys_intro: "Refreshing all keys from the keyring of list %{email} resulted in this:"
   pin_keys: Keys pinning
   pin_keys_intro: "While checking all subscriptions of list %{email} we were pinning a matching key for the following subscriptions:"
-  key_updated: Key %{fingerprint} was updated (%{states}).
-  key_fetched: Key %{fingerprint} was fetched (%{states}).
+  key_updated: |
+    This key was updated (%{states}):
+    %{key_oneline}
+  key_fetched: |
+    This key was fetched (%{states}):
+    %{key_oneline}
   import_states:
     unchanged: unchanged
     new_key: new key
diff --git a/schleuder.gemspec b/schleuder.gemspec
index 40d4a0e..a0c489f 100644
--- a/schleuder.gemspec
+++ b/schleuder.gemspec
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
   s.homepage     = "https://schleuder.nadir.org/"
   s.summary      = "Schleuder is a gpg-enabled mailinglist with remailing-capabilities."
   s.description  = "Schleuder is a group's email-gateway: subscribers can exchange encrypted emails among themselves, receive emails from non-subscribers and send emails to non-subscribers via the list.\n\nSchleuder takes care of all decryption and (re-)encryption, stripping of headers, and more. Schleuder can also send out its own public key upon request and process administrative commands by email."
-  s.files        = `git ls-files lib locales etc db/schema.rb README.md Rakefile bin/pinentry-clearpassphrase`.split
+  s.files        = `git ls-files lib locales etc db README.md Rakefile bin/pinentry-clearpassphrase`.split
   s.executables =  %w[schleuder schleuder-api-daemon]
   s.platform     = Gem::Platform::RUBY
   s.require_path = 'lib'
@@ -22,7 +22,7 @@ Gem::Specification.new do |s|
   s.license = 'GPL-3.0'
   s.add_runtime_dependency 'mail-gpg', '~> 0.3.0'
   s.add_runtime_dependency 'activerecord', '~> 4.1'
-  s.add_runtime_dependency 'rake', '~> 12'
+  s.add_runtime_dependency 'rake', '>= 10.5.0'
   s.add_runtime_dependency 'sqlite3', '~> 1'
   s.add_runtime_dependency 'sinatra', '~> 1'
   s.add_runtime_dependency 'sinatra-contrib', '~> 1'
@@ -32,6 +32,7 @@ Gem::Specification.new do |s|
   s.add_development_dependency 'hirb'
   s.add_development_dependency 'factory_girl'
   s.add_development_dependency 'database_cleaner'
+  s.add_development_dependency 'simplecov-console'
   s.post_install_message = "
 
     Please consider additionally installing schleuder-cli (allows to
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
index 4c8d38a..c572f86 100644
--- a/spec/factories/lists.rb
+++ b/spec/factories/lists.rb
@@ -7,6 +7,7 @@ FactoryGirl.define do
     subject_prefix_in nil
     subject_prefix_out nil
     openpgp_header_preference "signencrypt"
+    internal_footer nil
     public_footer nil
     headers_to_meta ["from", "to", "cc", "date"]
     bounces_drop_on_headers "x-spam-flag" => true
diff --git a/spec/fixtures/expiring_key.txt b/spec/fixtures/expiring_key.txt
new file mode 100644
index 0000000..e514153
--- /dev/null
+++ b/spec/fixtures/expiring_key.txt
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFmCxIEBCADUiLMwu7I/EuyyNlidrllyVt2GkXVwSa1yIR3nA1KyDqizspih
+SL4eVaZOvVWyJaGxIFbmG5WHCQi0YuuDv0jcGYT3lmyPumrDYyaU5GHm05gmZ+R4
++oSjJG/v5z7L6au2G/+Iw5pQg819VviJ6po+QwmQeUkHd7xhiniPq7aoVFcoltgE
+bQF8sPWJ1jyFnTmL36MECgAP9MKPfcSuHzUWJxiDSo8Siqaf7uUY7F2Gz9pBYuiv
+g6PJqnagtjZGPgxKqQak/IRvPjk6WWIGpaO9fBl1GQ/W3iuhkObBWuHNdErygQX8
+0VDc/wRfIeFsMjaPQ/nQOALgzsTGZWpdO/T9ABEBAAG0JmV4cGlyaW5nIGtleSA8
+ZXhwaXJpbmdrZXlAZXhhbXBsZS5vcmc+iQFUBBMBCAA+FiEEQh+/cZBkATZ4hZPN
+num+WSnKzCAFAlmCxIECGwMFCSWYBgAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AA
+CgkQnum+WSnKzCBnQwgAmCy016/VxvLHSZ45TuYO60dGfrmu6nA3/F2qKjXsszlw
+UL+7jiFZZzLRQF8GJGnHJnhg6mCz8yQKam8pOqaYCqVvc19GyzPdQ22uiOA0QcuE
+0kHd/ncAEtRJ3q+yeb7rcxVlHDHx2VpzNY3d2aRsgSYin+iAWKROUt3efZyTquzJ
+etOl6SvbwTv+qqDmrEj/zNQmr1Dic+KtLvK8ClyXrfZou5wCGsX14fKk7xkhNPBS
+f2oCaPjMjGsh9gZ+nZfpv+ppTAbZRPSKHuQrWksFGGxDTMigQORNtwA2SBljmL63
+neqajK086P3U3dZm4lD4dPCG/V2VVYgLsooCzJZUYbkBDQRZgsSBAQgAx9bBVzuv
+AlLbPIqHyXsjurosvoolUKAjdWFaGCK1lZHdy3RwuVVdgWA7qXpr/T6sQ0OzxaK/
+HT/1G0haCKd3zC2dD3FUQJHzIPXqE4x36a+PF/qq2vVe43+8Lwz2ceopHprHP7Sx
+anqnagiV/JQNJna7WTk4RM/61oHiBNWyusEhW2vynTs92ltEL4Esh3BKpyQ6mmV+
+DLGalraMSDcuGRzwQMTlRrXygxq2WHc9px3M3PfMBSj86ouhSOkwOPI3eV4XPzB+
+uXxwcZTsUxPm8BVVt5OAmrLaaX2K/AWEsZlVdSJpZrFD2WMMo3YRKrwvM8AHhuQO
+FJKaHPibi+a3zwARAQABiQE8BBgBCAAmFiEEQh+/cZBkATZ4hZPNnum+WSnKzCAF
+AlmCxIECGwwFCSWYBgAACgkQnum+WSnKzCAwMAf+L5ZFRTgha0AKnmWTR7JA2weg
+EPPDt33AHZFOCYcxuHm8TPgl4OuSvoxxWovCm56/nIiWnqojNVlWCQxAHHQmC1dA
+xJ7EDqdqCqVvRyRCzUsONICKeHdAoNA7TWw/DF+JZn2l+ud2w+EiQt9qy3sQXZeG
+SOG6s5kSPxxuwPmmMmCu5u+QI4elcHaw9RLGFbwlO4VlBS/n0k6P+UDY/4Eqkyi0
+L7GCFtw3ZKjRR24MKYdGRbv7hpzvp6JyrbYA9H4wGJir43Dk/EH7Ul+4Y8FcHFk4
+FU9yPzi0dW07tKD29F0GxRdqtIs0rWHIbUJVskDwDhJdteIsO/bTuKN4tZvtcQ==
+=c/Mw
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/spec/fixtures/mails/qp-encoding-clear.eml b/spec/fixtures/mails/qp-encoding-clear.eml
new file mode 100644
index 0000000..e3ca166
--- /dev/null
+++ b/spec/fixtures/mails/qp-encoding-clear.eml
@@ -0,0 +1,25 @@
+Date: Tue, 11 Jul 2017 12:00:40 +0200
+From: test at localhost
+To: schleuder at example.org
+Subject: Input Test QP encoding broken
+Message-ID: <20170711100040.GF7104 at blabla>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+On 17-07-11 11:59:56, schleuder at nadir.org wrote:
+> From: foo
+> To: schleuder at example.org
+> Cc:=20
+> Date: Tue, 11 Jul 2017 11:59:49 +0200
+> Sig: Good signature from somethingsomething
+> Enc: Encrypted
+
+> foo foo foo
+> foo foo foo
+> foo foo foo
+>=20
+> [1] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=3D867031
+
+https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=3D867031
diff --git a/spec/fixtures/mails/qp-encoding-encrypted+signed.eml b/spec/fixtures/mails/qp-encoding-encrypted+signed.eml
new file mode 100644
index 0000000..60f6783
--- /dev/null
+++ b/spec/fixtures/mails/qp-encoding-encrypted+signed.eml
@@ -0,0 +1,65 @@
+From: test at localhost
+Subject: something
+To: schleuder at example.org
+Message-ID: <381fb765-1e53-4578-bc93-bff7e689cbaa at example.org>
+Date: Fri, 21 Jul 2017 11:50:35 +0200
+MIME-Version: 1.0
+Content-Type: multipart/encrypted;
+ protocol="application/pgp-encrypted";
+ boundary="OguUqTN7k1TEvsvAHoUI3Hv5qStt1SOH3"
+
+This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
+--OguUqTN7k1TEvsvAHoUI3Hv5qStt1SOH3
+Content-Type: application/pgp-encrypted
+Content-Description: PGP/MIME version identification
+
+Version: 1
+
+--OguUqTN7k1TEvsvAHoUI3Hv5qStt1SOH3
+Content-Type: application/octet-stream; name="encrypted.asc"
+Content-Description: OpenPGP encrypted message
+Content-Disposition: inline; filename="encrypted.asc"
+
+-----BEGIN PGP MESSAGE-----
+
+hQIMA691X8Gl2MArAQ/7BrJwnNXjz+DhuRAyShy4LttCKX0RRG2rV2FegzYdfd40
+oB9ZPXtyL8L9fQ3uwUYpiexCqbR1yYNqPRkRZeHBOAVtHiUruZvuL+xq6yBXq3nt
+5Cz/TqN79B1G1+YyX+m5DOwURfiV104lBtfVE2g+XIG/+HGBBuviRT3VJrPQw1ld
+oVaQNf6KQORaUwYUkLxlX+M1MdPxA7MxKmCxVBJpakGtgKTihUcE9u0g5r3kYQke
+qyXglGu6RaFtztk5+JSrwn+z59Yjp+uD+qdYdtvXCkPHNWABT1CRwy2c7zUExWlh
+0sCxOwi6YeNJOBP1U2ceMNEHTe2XOarY5HOL1TW+bH8Ta1lCvGtwoZrwMJuDDpW0
+hpnE2YZmjh6M/WV5+ToJpcw6u5K2xBVB1hLWgLf5e8H9tVFhBywvjXt6Q1PKcMk1
+MfqAfYN22UYWbpbZ+AABccR+bAyECPspYbEjiZ9rjRDaDyjc5m6Jq4ctqy1ETtCA
+or/0sf8a+8Cxc9+tG3iaQ1f63LJTm914GJQfBb1Yi1PoEPOPxveUBcC88EVaaNvP
+PdnRx+L633FDDT2BHiYcK1/dD1BVjvZECtRK/522Y46epJi7E+U7b/RjOYcnfS4z
+ob344qvOhN+m0RJmEZVbYfr6UhxPMyQcDcX4kzpugVr/nwIk3mh3OBUuHeTovALS
+6gHa460vxp16neb5thpp96g60VC3R93KXTCPyfg5XvkziKPw/UM7vmz/Rqb4tOOb
+eMcsH73E34ONqfcPzX+Lrg7RqXhK9EZ5iuU3bFIEu1Xp0ixlXGPotyY/VR8P/+zu
+Hd8C2xoE8o7SeqVIf7UIViPV1fzB/DxHwa1Q+5rJMoDvP0CTqgC5YaRw24ab7Zbh
+GfKAPoAEiufwgAtsVNE9yF7+fb6U0AZ8qh0Jos+ksCA6FGOflnxwqCEYrH9hT7n6
+kAeVkoAoWH1YtNTQerMIASOOpjYMHu5AQnJ6RFOKDvYy+SQycTLz0DjkCCGakBPv
+ToyndsuGZRsnpP7ctO7CgtpCxFq8YpbZ/YzZ8aOf3UjW4K7dB4sgf2RrSCdCevK/
+8tDB/0a/t45KQoZiTVwOmWp3neL+r6rhelgAYKxV7fdIHn5C/ROH9Rm3m0Fua2mb
+onuelGo5bdndcl1BrkiA2iv03reOR6ID68JFJvp4KvZflu898jB2ZIkWw4vYaHDg
+Z5TnI9LlnJUqVFXn9SF3x8TkRdJysw6w4el8GFKzwxe7/y0CfH9MCCUTQ3pdpfna
+WTs00ElvgsWvIE8Nhz2cRnWfleex3/LJQhm/K24iQL45xR3RoGaO93KrbBZkUQp3
+0Nu+laHElEyA97YGxs36/3rinT6EMrq6gATrNHaXM94zPQR2g7+ooHTUR/RCvRLM
+80V5cpgcGkqT6Yy+wFDsTFaMJdH8ZlrOSdtthlePGqx11gvFOL8AJpGNMOq05OuV
+xcS7XpirPqgrblLdvgOQFM6D70nJ7g0HlMPyAm45+PyHPg4qVYXj91441mbnZfSC
+QUTtnJBUEp9ibpzckDn1i5ZYPEZ6QmIWfPc2heO7w3wEznwknTHfh0B2zjNAm9/B
+riW4xSvfIfpGyZnWrW/fGGYXYegiW0ljh49qBnTzEwSmr01YUoIl4mPA/dDou3RV
+WD/MgfvN9cYy4mZBMmwC45gwkU1gUELGHMfnOtLSdJ8MbOiW6CdIlvesO4KdrDdC
+rHrVBxdMXKSk1alcRi3KXzv86yF3q/XWyzC/BGVE9WF3DuG90NadnGDTP+IcGvy8
+fyaQhUIZ0AM/XawW/y/TBTbb2mjti6FFMsMy5aTg7OJO9kCgdkjgihQXJK1Y3jwQ
+Zk/qsvRDZc5u50z9+/Ln3OKoRw1V7dNTYwn30iiQ0jXWN3qfrl61aB3EprJuoCCO
+N97Jyq+s3aZNwf73VaIqjcKtIt9/ASvA2RczgcBzANG0C9GCeKXianQz14Qr0uvv
+8wG+thSAkGJgFlBEMfKXTYaRJnp+K0yjeBAP/JKVyltUYfamXLV9XLHn8sqsGwgL
+5lgrPeq0K7vTzCuhdWrP8kbACFvcXzJVi6wJ6q9r9w5sOnPIezjOX+42so6VB/lB
+j3MMeAdEIkzHuY3JrUseYp3O+/xjQzKw/8ep8IXL53sq+nLHeqJotN5geMVHJlIe
+fjKBmPwNygUETOV/8xfmjSqbQ5mLiy9BwtwUkRjkPV7iSl5GU4zreMEO79E2dWfL
+xzaqyAiBk4um86YPQBklXXFJlvyd9z/RJZiiSEdUcuF8NAtQhE0cwrkfK4EGeftT
+Xe7gviDKl62tMcXuN8d5E0RUeVdCGEaSQQy7
+=QAIj
+-----END PGP MESSAGE-----
+
+--OguUqTN7k1TEvsvAHoUI3Hv5qStt1SOH3--
diff --git a/spec/fixtures/mails/qp-encoding-encrypted.eml b/spec/fixtures/mails/qp-encoding-encrypted.eml
new file mode 100644
index 0000000..f349df7
--- /dev/null
+++ b/spec/fixtures/mails/qp-encoding-encrypted.eml
@@ -0,0 +1,47 @@
+Date: Tue, 11 Jul 2017 12:00:40 +0200
+From: test at localhost
+To: schleuder at example.org
+Subject: Input Test QP encoding broken encrypted
+Message-ID: <20170711100040.GF7104 at blabla>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+	boundary="4f28nU6agdXSinmL"
+Content-Disposition: inline
+
+
+--4f28nU6agdXSinmL
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--4f28nU6agdXSinmL
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+
+hQIMA691X8Gl2MArARAAreFTl/Yz+V0YBDfl54Zz+S3rENPDKXEmNJNzcYoLIEuH
+beoeWMHY9nzD7QkaNaeNL3foK9gr62njvrf6KsQVgnoUE8cQFhk2FJkPoylWGAnT
+2zI/dZLpMit/J3WSG1U02C/lOXN7M1kuvx7O/tGiKRtudyCwGO+qrAp/BCEIwBDz
+sJcEo9HAU4odV1oPFGDDw+y+EfOmTj+afplsSft0hDmI+hWnkjaWKCxhmR5BEWr2
+fBkaAzH4rMls8g22llZrL3Rens4UT/htOMiDXoxBQ4oE3P+sb8WesmpSJwU77y3J
+7DBdwBfI0q4kI3EjOBMvOQPLCYZl5+3LckQRl5QGPaBC+BaeAsXS2dEPRl5Xw1Ae
+ZzxTgusV7V0hDhNG3z3SzmczfUZ7+6OQ7HsOhHAVa3KmuWpaw8Dr5s2n6AhzZNGs
+A8HmWQETa9DxGQTKrcijB5LyIFTo8d5Hc144En9demO0ApBd3Jg5lhkAr+vAL/z0
+OkL35vosRWMncHx2GB6ido7IoMz4zXiKW34ezXJWvH5SwRdjOuy6WMvOzGToxJcn
+fK55f/xoq63N/uhHuVy8BH56AjI5OAfEowZ1jb/pGGRGM36JwrdwInbwSMU/X34r
+6ERESKrDqRvk27u0C10iAMFMfIdQVlfJN+kKXxyKDxOaukbSexnaFBNyi07b7DzS
+wLUBqDDjF8zXjCQNvOlqcBrVht3e0H4YqUFPfXy+fuGYL6FWobjuUDN9ZYbcuDwu
+w6ioBRoFmUwvOzTI9S40+JDR8RcFPbZuYYzBEZnqNJapsy7XrvXQUXlmOB7klm0s
+4m/d5edXlp93KqbO+oJ05Bse88cXvfDIrRKkV+YSWOB9VxAES3SK79QQRYlJlEdn
+zzcmSt8ODewc6WfO93u0gG/2xCftEHpi/iHFlgrzSVuZMa0Vn5AXva819aJ3t9D7
+OYKEp2IARQHSB30W7iIOK/cyS6ymE27137uETNQ7FinES9929E9WCd+3vk/lSZLr
+DnhSuGSNRV36srVsN7SkbbrvfF6iipJDC3Y1GQVmngpu6nU1r0/koqGkeIJr7Sao
+mGIgpVBBZYbhS3LBw6h87gtYEbKirwNLuP+WHY2uQkLIBCv7/ugrATHn503nwwEp
+GNFu+Bhh6FQZhMfgIlSHpV5jsHeMQFqy/xgW5VgnWhqZAEEKW1AA
+=IB9S
+-----END PGP MESSAGE-----
+
+--4f28nU6agdXSinmL--
+
diff --git a/spec/fixtures/partially_expired_key.txt b/spec/fixtures/partially_expired_key.txt
new file mode 100644
index 0000000..3c9079b
--- /dev/null
+++ b/spec/fixtures/partially_expired_key.txt
@@ -0,0 +1,36 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBExlilkBCACb2AQyclf7latAIE1kCTfKQ9jmcKyf959ymyhzoeNmBDpKjILC
+7MOXtICo/V/xAzhWBK/vT9+56brGUBTugnW3yK+zllQprI3kIYaRS1SrbmKVwVse
+9qLVUL1BssohFaEeQqT4MNh62ziJymqCguGEGXpYlEqzEDTmmhTANiPKRBZDrdfq
+3FU3OJUMTGzuG34mKmXMRr0azprF228LUujMMKyWhG1hxh3El04C4jPuMSbaVcwN
+E2rgIg8jaNAQuSyXkaprPZ8/nRG8UFGRtCMEIEh6Ou6KybF1NI9LQKCwcsGcLHKU
+2u/8vOCExxdwl9Jjlqmof4FQV7bT++6SC0n3ABEBAAG0EWV4cGlyZWQgPGJsYUBm
+b28+iQE+BBMBAgAoBQJMZYpZAhsDBQkAAVGABgsJCAcDAgYVCAIJCgsEFgIDAQIe
+AQIXgAAKCRD3Gj+EEtg4iV6yB/4uDLoN1+TswtGjpUlu2CyjHe7pb05dAU4sWfTV
+I+fxBuyEo+cf/23nOeoGyltBDR0heSg3TIfXQrbWD4WoVsOXPaT0fq6UEzeadkmn
+A5NN3PGkv46o3ZSF1ltkY9ybMgnmRLHYCojSu5bSBMRVyurr0ozwNRPtFUTka8Lj
+wxiwDJ394D5y6PjL56FPkUdKydzFGV2ptSKsqyAJvMBeGlQ4I6TpiBx0Lz2A1Qn+
+4uXgTVPqgalC5YKTTTjOfQcieOOeqtI0LHqDpS/DIPLnwTUCN8OL2TQIeDudm3YI
+P8FCKvImh840vTbpgFSgQeaJzJFv9UrloNyyvbVtaeoxnxoBiQFPBBMBCgA5AhsD
+BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgBYhBJh2nooQkfNr2IQD7PcaP4QS2DiJ
+BQJZ6HpOAAoJEPcaP4QS2DiJfQwH/2+mSIWXyc8hJyFyPiKFsiwb1JDsAV4DSSUh
+x+KYZsX0biBx6DRBf0IXt2QdrbowPEo9oSnpiiy4tbnM9N2ccvrlls63OKzAXDCF
+ckdB9VmlDRZkPPo9It1EzX0E6x9BXmGXFl5yoGPcsXMRmf2CyQxMDWB7yNfuGFCT
+QC+6qsNJtP8sOk+EZz/5elSq6d3F+k6YzfHrCvRu7mr5ugBtEsp8epSo5gswjOIi
+GAhHX1AaZbxwvWAdR8B3A6RbOqQVZnzsiWbpOg1/Bgg0Td33sh1DdXh63ouuHIdM
+baqT6t8gJbHBaTmfFoT7WPEA8zCDfqU0xeP7AWwSs4KITXC+KoS5AQ0ETGWKWQEI
+AMMZt/RMSzkcOltQLvy0l60ZaKmZBFOryBL10OgqMbma+WkUBE9MAm97CAjsMgJc
+y1Vjw/x4VuxkMYwRN266dp260O/I/0n/C6SdgcmQTmMl5mMQrKh+tYzUEfEdvZuQ
+1TSGvwRU28SFWMBvrZwk+Pl6aL/dSHKcugmus74BlKv0GrZBbfye1JTFNcztVsZR
+HUxZlKgn6ASD8h5HYcnOILJvWaGW+j2lmolP3n1s1VD/FTg0e1hxrlxXdfzwCV3g
+PDPYcz48gFuyLPcLd8gw2tHFG8wJTg+CyNJkWhHFx0NFPk6MHE2BDhSZZAfSxMXD
+1PHwHY63C6C4bBpchXcWlEMAEQEAAYkBJQQYAQIADwUCTGWKWQIbDAUJAAFRgAAK
+CRD3Gj+EEtg4iaqeB/9BC3RLq2Un04ySeYCvak2D4NCQKpM1pKT+JCPiV4YbCSlF
+WgflUVNnSeSZmNwNNOsKOPmsQNpGjxDfwqwHWmjw/bkAGgrRvbysfDwjcvYHKXX4
+gHv500pXIUlV8cBg5Cxsj+n/Yp21lkhunGdFUdj5WW7an9/Hcmo+aoKJpUxxQXDn
+lCNIpoj6iZ+c3RYbKsrVG5v3bktyHS9mTFXCZOx7uq5+DSLBNEQzL1Vl8xq8yGQC
+0zYCyIQbyPebBT0lB8GReq1bEbZW+2wEnSstp+HmxHc2CO1Ha9tJ3MlEJXbE7jPo
+OuHc+6DdF4eZwtQVMM8T6+Z4f7GkRnaeG81EUOGZ
+=xAyq
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/spec/fixtures/revoked_key.txt b/spec/fixtures/revoked_key.txt
new file mode 100644
index 0000000..0db6629
--- /dev/null
+++ b/spec/fixtures/revoked_key.txt
@@ -0,0 +1,33 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGiBEjVO7oRBADQvT6wtD2IzzIiK0NbrcilCKCp4MWb8cYXTXguwPQI6y0Nerz4
+3YLDaGdb7idJAybemG/BHNVUG90XnCE5XhKKzcjFfklCly+RzRDhJqfBn7z8eH5e
+dsK6J0X1Vgeo02tqA4xd3EDK8rdqL2yZfl/2egH8+85R3gDk+kqkfEp4pwCgp6VO
+aEGVKE6G3fX1R6bSUNJybI0D/jhUEj/HXCs44AF+zIKKSzTCwzfdvjHjjs2uoF7t
+UJa89W7f0EIhFa10RizIfCsivmPxXPxtCvIYGPrssZdFTSgrmy9jU/9pivueyFWY
+TM8iKelPanlmwP5tzXJt4LRzU3OpsneUSyu60Tpad1trK/2QeqFRNYFVr7OFnGWB
+rME5BADOU8Zz+Xc65uKcBtjQXd2razdSuil4fgjHicmIwNYnD2aj6lnc+fnfuAYx
+NPVxUoWfCS4OaSq09OANwFzyWHRGyS/IetaP1kIaxwkOe1j6r0IiF6ZFpZt1DFYF
+ikgnJBCYDvG95j/8M6Ta05KERkxbjq2QSW4F4s0ax/Au1tztc4hnBCARCAAnBQJM
+ikhtIB0BUGxlYXNlIHVzZSAweDA2OUU1NURFIGluc3RlYWQuAAoJEAmKyDpMACjp
+D3sAnjJSaWwbbbPzYug/Jkg+G4Qy+NlKAJ4geMqNPlyPzCci1vqGpTMZwbPm8rQT
+cGF6IDxwYXpAbmFkaXIub3JnPohiBBMRAgAiAhsDAh4BAheABQJKAbofBwsJCAcD
+AgEEFQoIAwUWAgMBAAAKCRAJisg6TAAo6TZkAKCVGY5i5Vty1AHB+Oxw74ek33cP
+lACgjiUAdpcJ0Nb0xjQQT1JPW79LIe6IYAQTEQIAIQUCSNU7ugIbAwcLCQgHAwIB
+BBUCCAMEFgIDAQIeAQIXgAAKCRAJisg6TAAo6X7NAJiXMEyWnG743jgn7kapIylO
+wcAQAJ4742pWNW+u2pyrA2tSTnB5B7g9ILkCDQRI1Tu6EAgA0LZj6Cy3t6vCvbyg
+8tNXEBA3fED2nAFnmsMmQhlOhy/yqpa3WFnxZhcqi8v81E9bCQrhh3hGIi/aLdJT
+6ENxAvDs2vdNAL3Tz3hB/bTGtSLLbgU64xVuMFD7nNeT17GRmabm8utEnKgf6o/c
+n4+nS1YPQq64b3CpId7Va67nF58fxBGuNgFy7uNIN6zqblHgPWD/cr2hR5hPX9kk
+xMgS42PFBqoJZ0I+uaTZRf6pGlsEW9OPIfojK1ttRJKLiykwzxZHRW2qVdl+SiWS
+HH4TtcxG8HNWShdybimBy4fGM2bIk3P6d/t3gQL5/Q5LEBScoDNOYqqUCXcpdGiA
+Ji+CEwADBggAjuxoMm6Mc2ST4B0RkiA3PE3rCFhoc4gzjksTNyYuZj9w9JSADa3V
+l73Su4D4YByCOYxVpdde703dFIlLf4/Gbzdx6ejj5g9Nk3ARR3LtEJsno97HIYQK
+uM0iyTW1cajAJR7gI+K+YQ07G8vFV81t4DlPD9dHJPV6blgy/HgapnthWJBB5L6b
+MaAwk+inqbkisyaxE4Ov6mOULqhzyDkGZXlgeaa0wUcyg28WRulPGOG/xICpuM6a
+FNRKvuBbrX2RH2AiLXwAfclrIe7x7a2RgQcb1tcvfxoQIP7gwAGtsP98dPc1pkma
+0GMMcA57HTRKe/T/ZrAVqF3aXdBL48Nc3IhJBBgRAgAJBQJI1Tu6AhsMAAoJEAmK
+yDpMACjpNwsAnRkZCkPV+RkZmOtoc/UgmZ7AtI3mAJ4vDlEFfTV315Uv73jwVJzi
+9PDUAQ==
+=Latx
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/spec/fixtures/schleuder_at_example_public_key.txt b/spec/fixtures/schleuder_at_example_public_key.txt
index 211da08..e7423ca 100644
--- a/spec/fixtures/schleuder_at_example_public_key.txt
+++ b/spec/fixtures/schleuder_at_example_public_key.txt
@@ -1,3 +1,5 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
 mQINBFhGvz0BEADXbbTWo/PStyTznAo/f1UobY0EiVPNKNERvYua2Pnq8BwOQ5bS
 qvmWTb+9Fe9PRpmmqqKKeKjMX9yNjYZweqZ2Tda6C9B6shLme/bWotoXYCEjmP4t
 g9uWJmu2x+UHH+SSH752eDlTOtz9mZqNjcKlwvZtHcfu3FwvPQ7xqYt3f2Q/e8ES
diff --git a/spec/fixtures/signonly_key.txt b/spec/fixtures/signonly_key.txt
new file mode 100644
index 0000000..040fce1
--- /dev/null
+++ b/spec/fixtures/signonly_key.txt
@@ -0,0 +1,18 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFmC2g4BCADDRv16LnKRbVT9uyjtNkReqM0abvWUKZOBGYr1JV8t8hEeMKwv
+BpPwh1AZyw+Ihhn/BybwyBDfqmInKWE81bGUl0ZRTaX2i/5JP7uTIcn/7DY3O4rN
+Hx/h/Xworqq34MhdoxJCsxFlaEyPaNg9bNNVknoKDRpPTMWBCOhLlvJTr2Lr6gbk
+bDt62fRR6qrwA7Um28jz2j0Dn0bFKv7mDFlQ4zVL89z93oa5G5JRztogWv4FyawM
+slSrvLhMt+Jw8HOL2w0vyPuZTcITgy8mcyajF2/NnP6aW5p6ujXIw4od3gJtDUOI
+Uec+ZMFSDvD1jzv42ksHz5D4SkuWWrtffwfxABEBAAG0H3NpZ25vbmx5IDxzaWdu
+b25seUBleGFtcGxlLm9yZz6JAU4EEwEKADgWIQSxzYuxXCZzxr/Y+ktwss8p4BrV
+PgUCWYLaDgIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRBwss8p4BrVPgj+
+B/9hs+NMmdjOM/3KGrL65qVKwAe/zR0xVhmnT+kg9XkCuH1dKT2vae5zkzvU883R
+DTwk5nFpKFuklYDyTtpOxZkmNXzYIBihFdqWLdg9SO/ydXwFB5dcsinAadWYTNg6
+NS6hA5hG7Uc3zush8DzvUvR9JPdKTXLZ4zKNZLJ/SdLQO+e3lgDkfkJ9RH4VNFja
+Fys4nXVLOAnj39Yngcz6e1xngvqM4453/XgHtxg9AnDj4w09LgdoVfonPwKiLiZ7
+f7eFeaHdQr+OWI/0A4Nghzd1SHnBSvlDS7XRiuiu1SYkDWK6M8c7p+It3wHAUeG3
+tXmKTndc5oY20DAh14ez7kt6
+=SgbM
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/spec/schleuder-certificate.pem b/spec/schleuder-certificate.pem
new file mode 100644
index 0000000..1c9ac1d
--- /dev/null
+++ b/spec/schleuder-certificate.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDeDCCAmCgAwIBAgIBADANBgkqhkiG9w0BAQsFADA1MQswCQYDVQQGEwJNVzES
+MBAGA1UECgwJU2NobGV1ZGVyMRIwEAYDVQQLDAlzY2hsZXVkZXIwHhcNMTcwNjA3
+MTU1NDU2WhcNMjcwNjA1MTU1NDU2WjA1MQswCQYDVQQGEwJNVzESMBAGA1UECgwJ
+U2NobGV1ZGVyMRIwEAYDVQQLDAlzY2hsZXVkZXIwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQCkEknthf/N325Yo2Vi+7JAXyewBmgAN+fQapB8KAba8BrN
+8TIKi7otJ4GA2TNyYtSlXnEUslXu9YHSevDgrJpnzE0pnOmbDA21yyAZk1kG4NMO
+fnxw0pjyLIcOF+Do2PCI6K5ipKUZParGiBcdaJpJhRkePw5ILpGu5B3VwgLlQ+H/
+vl0dC2+dLrEIVrTs5aUw5EFx/CS5i0KnYDo0K/jvDeFN2xa2tlfVOcQ9ZtVwe+Pl
+Rl2Kue/RcyIwfE/0M9ijys9xC7lrfmJdeBZ0i0sRoz/eTH5BAUwAGQeMAX4C58Yz
+qHg9WxIPUsFCvHGqIqg8IWjvfX8s2u6HEpk5HWcrAgMBAAGjgZIwgY8wDwYDVR0T
+AQH/BAUwAwEB/zAdBgNVHQ4EFgQUB/5P+h1owyWXwRKYnnsFrUyyYEswXQYDVR0j
+BFYwVIAUB/5P+h1owyWXwRKYnnsFrUyyYEuhOaQ3MDUxCzAJBgNVBAYTAk1XMRIw
+EAYDVQQKDAlTY2hsZXVkZXIxEjAQBgNVBAsMCXNjaGxldWRlcoIBADANBgkqhkiG
+9w0BAQsFAAOCAQEAHRz4AmJWrOe9ieIh8I+qAqUsXtX9Hgh/2RvlNTNs/NvSHb8d
+xgrncM2ULzWeq+y7Egj61+85x2xXSplgruUFW04Rql01fzBAFafJqk2SvHEzdLIx
+1mzHth/d7mFFMg0HYT4iiwhiklH3jrDJHT7EUunRQQEhgunWdXw57BiAppG2bW0U
+J9LHcGMsxjR6JxbgwQoCZRUBwiGAKd5P4dLc7sELx+wdjU+ChHNf2c7kzVi//Oa2
+Ky7jgEuQ0TWmCzAKtx8L1WDu8x0I1QoGrUV0mLVkoCt8nPjDhbxM4hQq/QgkNZ/C
+wK+nz/RKC99U6r8eaofY2MJA8lr1+69JfzkelQ==
+-----END CERTIFICATE-----
diff --git a/spec/schleuder-private-key.pem b/spec/schleuder-private-key.pem
new file mode 100644
index 0000000..80f25b4
--- /dev/null
+++ b/spec/schleuder-private-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEApBJJ7YX/zd9uWKNlYvuyQF8nsAZoADfn0GqQfCgG2vAazfEy
+Cou6LSeBgNkzcmLUpV5xFLJV7vWB0nrw4KyaZ8xNKZzpmwwNtcsgGZNZBuDTDn58
+cNKY8iyHDhfg6NjwiOiuYqSlGT2qxogXHWiaSYUZHj8OSC6RruQd1cIC5UPh/75d
+HQtvnS6xCFa07OWlMORBcfwkuYtCp2A6NCv47w3hTdsWtrZX1TnEPWbVcHvj5UZd
+irnv0XMiMHxP9DPYo8rPcQu5a35iXXgWdItLEaM/3kx+QQFMABkHjAF+AufGM6h4
+PVsSD1LBQrxxqiKoPCFo731/LNruhxKZOR1nKwIDAQABAoIBAQCYF5wQYzdOUOCp
+qk5CA7Cpm4ve0RF3oltyCFcHwNMaAZnXbs9El2JumUCjgLUARD17TqDk3qxqZ4uA
+4haJL3ey4OBmwt6KrBHJhBKtornUdnUv6nDQ5WiClmRb3CbRssjHIWsGZjnlvBSj
+FWTYDi94F7nBIBLNNt41kaFWlhK5E0fsPwd5efvNbZpJSwDPZ/iG1dwyZhCGmfu2
+HeziDRDxBNDRvPFdwj2l4Oty55vqLXJuQ4nHIHk+DkPd4RtLn8xLFLdm1iFbFSFd
+k7/0bqpzF3+8o0lXIY3jTKH/Z1C57NAW3enjVADRuqKNZjzIsBKKCF9u/ZkfCLMC
+EvPlyhYBAoGBANdtJ2O4pgBho6r4Pe/iyeSTS8knr6AU+ViQMfti6qHbBH918wCa
+ohj5tlEovZedwaKTB58hNy6U7CT5uVK5L8LKXqBYul97ONWlSumj2J++cAsnPCyx
+p+zYjUm3KI/jUx/vGccOsd1eMGvhYXWBkRU9JG4qL+12W2SgE7Q+XEWxAoGBAML5
+CpYt2JGE27HkQBnyaApQAARKShQhUrsHohApZpyZGkwbIqmmjEaCY1nqk70s1LLg
+nU1vmlnRXNetIhUcj+JtnHKJDqMEvZM/eFxQIXKdsVcD6WQf2u9ZU31TW8uH5G88
+eadEHCRrTe/xvEyHlDlIXUkEJwmJDF+36QZQ/sWbAoGBAMNwA4w0uGUgL5usGoTG
++uKjvt1/Y5WXcZ8nMjEeTD8Ks8nu98ZUgzqlUQHQNDCYrlMPkJqNR8K62IGzDK4/
+01Skw7Q0yuBUqfspOg082AoUexGjRrRFeFMnIwb9Y48mbQNLp9cvPa3XBZbZodE4
++qaKEcLgAxsrhT6E+1tKN+wBAoGAD5SBEREmzjIUsDlyGeCyCajs52rcUpF7H/Dz
+NWFpjrf5Tv2YHoBtkzDWKZhCKArOEGE8kLSLXAQL7Dwsjg1TPh/OMaTcI5C8aWjY
+AGBy28rYIgDxBIw7HYdA0bH4kuIQEgd+HSynJw3gE314s5Dd+lnbAnuvduaZs4hp
+uZR9V2MCgYB8mmk+KEiD2I3hgkBZSk6eKf1NJOrYO4xdwCb42XArERO4PtiQBdP2
+VfsXhPnkkrZSDJ3/vZGw+uiI7u5UD8fl3ATIRMeYYqyiTeFM4ELPToF6hDhtcf/q
+l0fkQB4Je9KT8zqCDkvytvDjTTXGrxDEPTEPL1OymX4SiIRJaRSEQw==
+-----END RSA PRIVATE KEY-----
diff --git a/spec/schleuder.yml b/spec/schleuder.yml
index 992da66..1d2bffc 100644
--- a/spec/schleuder.yml
+++ b/spec/schleuder.yml
@@ -1,9 +1,13 @@
 database:
   test:
     adapter: sqlite3
-    database: db/test.sqlite3
-lists_dir: /tmp/schleuder-test/
-listlogs_dir: /tmp/schleuder-test/
+    database: <%= ENV["SCHLEUDER_DB_PATH"] || 'db/test.sqlite3' %>
+lists_dir: <%= ENV["SCHLEUDER_TMP_DIR"] || '/tmp/schleuder-test/' %>
+listlogs_dir: <%= ENV["SCHLEUDER_TMP_DIR"] || '/tmp/schleuder-test/' %>
 smtp_settings:
   port: 2523
 keyserver: hkp://127.0.0.1:9999
+api:
+  tls_cert_file: 'spec/schleuder-certificate.pem'
+  tls_key_file:  'spec/schleuder-private-key.pem'
+  valid_api_keys: 'test_api_key'
diff --git a/spec/schleuder/integration/api_daemon/api_daemon_spec_helper.rb b/spec/schleuder/integration/api_daemon/api_daemon_spec_helper.rb
new file mode 100644
index 0000000..b29fca6
--- /dev/null
+++ b/spec/schleuder/integration/api_daemon/api_daemon_spec_helper.rb
@@ -0,0 +1,17 @@
+ENV['RACK_ENV'] = 'test'
+
+require 'spec_helper'
+require 'rack/test'
+
+require 'schleuder-api-daemon'
+
+module RSpecMixin
+  include Rack::Test::Methods
+  def app() SchleuderApiDaemon end
+end
+
+RSpec.configure { |c| c.include RSpecMixin } # For RSpec 2.x and 3.x
+
+def authorize!
+  basic_authorize 'schleuder', 'test_api_key'
+end
diff --git a/spec/schleuder/integration/api_daemon/authorization_spec.rb b/spec/schleuder/integration/api_daemon/authorization_spec.rb
new file mode 100644
index 0000000..b5d849f
--- /dev/null
+++ b/spec/schleuder/integration/api_daemon/authorization_spec.rb
@@ -0,0 +1,20 @@
+require_relative 'api_daemon_spec_helper'
+
+describe 'authorization via api' do
+  it 'allows un-authorized access to /status.json' do
+    get '/status.json'
+    expect(last_response).to be_ok
+  end
+
+  it 'blocks un-authorized access to other URLs' do
+    get '/lists.json'
+    expect(last_response.status).to be(401)
+  end
+
+  it 'allows authorized access' do
+    authorize!
+    get '/status.json'
+    expect(last_response).to be_ok
+  end
+
+end
diff --git a/spec/schleuder/integration/api_daemon/list_spec.rb b/spec/schleuder/integration/api_daemon/list_spec.rb
new file mode 100644
index 0000000..e71a937
--- /dev/null
+++ b/spec/schleuder/integration/api_daemon/list_spec.rb
@@ -0,0 +1,29 @@
+require_relative 'api_daemon_spec_helper'
+
+describe 'lists via api' do
+
+  before :each do
+  end
+
+  it 'creates a list' do
+    authorize!
+    list = create(:list)
+    parameters = {
+      email: 'new_testlist at example.com',
+      fingerprint: list.fingerprint
+    }
+    expect {
+      post '/lists.json', parameters.to_json
+      expect(last_response.status).to be 200
+    }.to change { List.count }.by 1
+  end
+
+  it 'shows a list' do
+    authorize!
+    list = create(:list)
+    get "lists/#{list.id}.json"
+    expect(last_response.status).to be 200
+    expect(JSON.parse(last_response.body)['email']).to eq list.email
+  end
+
+end
diff --git a/spec/schleuder/integration/api_daemon/subscription_spec.rb b/spec/schleuder/integration/api_daemon/subscription_spec.rb
new file mode 100644
index 0000000..f7ed4b1
--- /dev/null
+++ b/spec/schleuder/integration/api_daemon/subscription_spec.rb
@@ -0,0 +1,68 @@
+require_relative 'api_daemon_spec_helper'
+
+describe 'subscription via api' do
+
+  before :each do
+    @list = List.last || create(:list)
+    @email = create(:subscription).email
+  end
+
+  it 'doesn\'t subscribe new member without authorization' do
+    parameters = {'list_id' => @list.id, :email => @email}
+    expect {
+      post '/subscriptions.json', parameters.to_json
+      expect(last_response.status).to be 401
+    }.to change { Subscription.count }.by 0
+  end
+
+  it 'subscribes new member to a list' do
+    authorize!
+    parameters = {'list_id' => @list.id, :email => @email}
+    expect {
+      post '/subscriptions.json', parameters.to_json
+      expect(last_response.status).to be 201
+    }.to change { Subscription.count }.by 1
+    expect(Subscription.where(:email => @email).first.admin?).to be false
+    expect(Subscription.where(:email => @email).first.delivery_enabled).to be true
+  end
+
+  it 'subscribes an admin user' do
+    authorize!
+    parameters = {'list_id' => @list.id, :email => @email, :admin => true}
+    expect {
+      post '/subscriptions.json', parameters.to_json
+      expect(last_response.status).to be 201
+    }.to change { Subscription.count }.by 1
+    expect(Subscription.where(:email => @email).first.admin?).to be true
+  end
+
+  it 'subscribes an admin user with a truthy value' do
+    authorize!
+    parameters = {'list_id' => @list.id, :email => @email, :admin => '1'}
+    expect {
+      post '/subscriptions.json', parameters.to_json
+      expect(last_response.status).to be 201
+    }.to change { Subscription.count }.by 1
+    expect(Subscription.where(:email => @email).first.admin?).to be true
+  end
+
+  it 'subscribes an user and unsets delivery flag' do
+    authorize!
+    parameters = {'list_id' => @list.id, :email => @email, :delivery_enabled => false}
+    expect {
+      post '/subscriptions.json', parameters.to_json
+      expect(last_response.status).to be 201
+    }.to change { Subscription.count }.by 1
+    expect(Subscription.where(:email => @email).first.delivery_enabled).to be false
+  end
+
+  it 'unsubscribes members' do
+    authorize!
+    subscription = create(:subscription, :list_id => @list.id)
+    parameters = {'list_id' => @list.id, :email => @email, :delivery_enabled => false}
+    expect {
+      delete "/subscriptions/#{subscription.id}.json"
+      expect(last_response.status).to be 200
+    }.to change { Subscription.count }.by -1
+  end
+end
diff --git a/spec/schleuder/integration/cli_spec.rb b/spec/schleuder/integration/cli_spec.rb
index 65a771c..aa43648 100644
--- a/spec/schleuder/integration/cli_spec.rb
+++ b/spec/schleuder/integration/cli_spec.rb
@@ -73,6 +73,7 @@ describe 'cli' do
       expect(list.subject_prefix_out).to eq '[out]'
       expect(list.max_message_size_kb).to eq 10240
       expect(list.public_footer).to eq "-- \nfooter"
+      expect(list.internal_footer).to be_nil
     end
 
     it "imports the subscriptions" do
@@ -146,7 +147,7 @@ describe 'cli' do
       mail = Mail::TestMailer.deliveries.first
 
       expect(Mail::TestMailer.deliveries.length).to eq 1
-      expect(mail.first_plaintext_part.body.to_s).to eql("Refreshing all keys from the keyring of list #{list.email} resulted in this:\n\nKey 98769E8A1091F36BD88403ECF71A3F8412D83889 was updated (new signatures).\nKey 6EE51D78FD0B33DE65CCF69D2104E20E20889F66 was updated (new user-IDs, new signatures).")
+      expect(mail.first_plaintext_part.body.to_s).to match(/Refreshing all keys from the keyring of list #{list.email} resulted in this:\n\nThis key was updated \(new signatures\):\n0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]\n\nThis key was updated \(new user-IDs and new signatures\):\n0x6EE51D78FD0B33DE65CCF69D2104E20E20889F66 new at example.org \d{4}-\d{2}-\d{2}\n/)
 
       teardown_list_and_mailer(list)
     end
@@ -165,7 +166,7 @@ describe 'cli' do
       mail = Mail::TestMailer.deliveries.first
 
       expect(Mail::TestMailer.deliveries.length).to eq 1
-      expect(mail.first_plaintext_part.body.to_s).to eql("Refreshing all keys from the keyring of list #{list1.email} resulted in this:\n\nKey 98769E8A1091F36BD88403ECF71A3F8412D83889 was updated (new signatures).\nKey 6EE51D78FD0B33DE65CCF69D2104E20E20889F66 was updated (new user-IDs, new signatures).")
+      expect(mail.first_plaintext_part.body.to_s).to match(/Refreshing all keys from the keyring of list #{list1.email} resulted in this:\n\nThis key was updated \(new signatures\):\n0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]\n\nThis key was updated \(new user-IDs and new signatures\):\n0x6EE51D78FD0B33DE65CCF69D2104E20E20889F66 new at example.org \d{4}-\d{2}-\d{2}\n/)
 
       teardown_list_and_mailer(list1)
       teardown_list_and_mailer(list2)
diff --git a/spec/schleuder/integration/keywords_spec.rb b/spec/schleuder/integration/keywords_spec.rb
index 8c0c739..bbb30cf 100644
--- a/spec/schleuder/integration/keywords_spec.rb
+++ b/spec/schleuder/integration/keywords_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
 require "spec_helper"
 
 describe "user sends keyword" do
@@ -15,7 +16,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-SUBSCRIBE: test at example.org"
+    mail.body = "x-list-name: #{list.email}\nX-SUBSCRIBE: test at example.org"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -58,7 +59,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-SUBSCRIBE: test at example.org 0x#{list.fingerprint} true false"
+    mail.body = "x-list-name: #{list.email}\nX-SUBSCRIBE: test at example.org 0x#{list.fingerprint} true false"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -86,6 +87,91 @@ describe "user sends keyword" do
     teardown_list_and_mailer(list)
   end
 
+  it "x-subscribe with one attribute and spaces-separated fingerprint" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.request_address
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.request_address => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    mail.body = "x-list-name: #{list.email}\nX-SUBSCRIBE: test at example.org 0x#{list.fingerprint.dup.insert(4, ' ')} true"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.request_address)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
+    subscription = list.subscriptions.where(email: 'test at example.org').first
+
+    expect(message.to).to eql(['schleuder at example.org'])
+    expect(message.to_s).to include("test at example.org has been subscribed")
+    expect(message.to_s).to match(/Fingerprint:\s+#{list.fingerprint.downcase}/)
+    expect(message.to_s).to include("Admin? true")
+    expect(message.to_s).to include("Email-delivery enabled? true")
+
+    expect(subscription).to be_present
+    expect(subscription.fingerprint).to eql(list.fingerprint.downcase)
+    expect(subscription.admin).to eql(true)
+    expect(subscription.delivery_enabled).to eql(true)
+
+    teardown_list_and_mailer(list)
+  end
+
+
+  it "x-subscribe without attributes, but with spaces-separated fingerprint" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.request_address
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.request_address => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    mail.body = "x-list-name: #{list.email}\nX-SUBSCRIBE: test at example.org 0x#{list.fingerprint.dup.insert(4, ' ')}"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.request_address)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
+    subscription = list.subscriptions.where(email: 'test at example.org').first
+
+    expect(message.to).to eql(['schleuder at example.org'])
+    expect(message.to_s).to include("test at example.org has been subscribed")
+    expect(message.to_s).to match(/Fingerprint:\s+#{list.fingerprint.downcase}/)
+    expect(message.to_s).to include("Admin? false")
+    expect(message.to_s).to include("Email-delivery enabled? true")
+
+    expect(subscription).to be_present
+    expect(subscription.fingerprint).to eql(list.fingerprint.downcase)
+    expect(subscription.admin).to eql(false)
+    expect(subscription.delivery_enabled).to eql(true)
+
+    teardown_list_and_mailer(list)
+  end
+
   it "x-subscribe with attributes and spaces-separated fingerprint" do
     list = create(:list)
     list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
@@ -100,7 +186,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-SUBSCRIBE: test at example.org 0x#{list.fingerprint.dup.insert(4, ' ')} true false"
+    mail.body = "x-list-name: #{list.email}\nX-SUBSCRIBE: test at example.org 0x#{list.fingerprint.dup.insert(4, ' ')} true false"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -144,7 +230,7 @@ describe "user sends keyword" do
       sign_as: '59C71FB38AEE22E091C78259D06350440F759BD3'
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-UNSUBSCRIBE:"
+    mail.body = "x-list-name: #{list.email}\nX-UNSUBSCRIBE:"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -180,7 +266,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-UNSUBSCRIBE: test at example.org"
+    mail.body = "x-list-name: #{list.email}\nX-UNSUBSCRIBE: test at example.org"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -214,7 +300,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-UNSUBSCRIBE: test at example.org"
+    mail.body = "x-list-name: #{list.email}\nX-UNSUBSCRIBE: test at example.org"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -251,7 +337,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-set-fingerprint: schleuder at example.org C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
+    mail.body = "x-list-name: #{list.email}\nX-set-fingerprint: schleuder at example.org C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -290,7 +376,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-set-fingerprint: schleuder at example.org C4D6 0F88 3378 9C7C  AA44 496F D3FF A661 3AB1 0ECE"
+    mail.body = "x-list-name: #{list.email}\nX-set-fingerprint: schleuder at example.org C4D6 0F88 3378 9C7C  AA44 496F D3FF A661 3AB1 0ECE"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -329,7 +415,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-set-fingerprint: C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
+    mail.body = "x-list-name: #{list.email}\nX-set-fingerprint: C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -368,7 +454,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-set-fingerprint: test at example.org C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
+    mail.body = "x-list-name: #{list.email}\nX-set-fingerprint: test at example.org C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -407,7 +493,7 @@ describe "user sends keyword" do
       sign_as: '59C71FB38AEE22E091C78259D06350440F759BD3'
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-set-fingerprint: test at example.org 59C71FB38AEE22E091C78259D06350440F759BD3"
+    mail.body = "x-list-name: #{list.email}\nX-set-fingerprint: test at example.org 59C71FB38AEE22E091C78259D06350440F759BD3"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -444,7 +530,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-set-fingerprint: blabla"
+    mail.body = "x-list-name: #{list.email}\nX-set-fingerprint: blabla"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -480,7 +566,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-set-fingerprint: bla at example.org C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
+    mail.body = "x-list-name: #{list.email}\nX-set-fingerprint: bla at example.org C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -513,7 +599,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-list-subscriptions:"
+    mail.body = "x-list-name: #{list.email}\nX-list-subscriptions:"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -548,7 +634,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-list-subscriptions:"
+    mail.body = "x-list-name: #{list.email}\nX-list-subscriptions:"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -588,7 +674,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-list-subscriptions: example.org"
+    mail.body = "x-list-name: #{list.email}\nX-list-subscriptions: example.org"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -622,7 +708,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-list-subscriptions: blabla"
+    mail.body = "x-list-name: #{list.email}\nX-list-subscriptions: blabla"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -657,7 +743,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     keymaterial = File.read('spec/fixtures/example_key.txt')
-    mail.body = "x-listname: #{list.email}\nX-ADD-KEY:\n#{keymaterial}"
+    mail.body = "x-list-name: #{list.email}\nX-ADD-KEY:\n#{keymaterial}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -690,7 +776,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-ADD-KEY:"
+    mail.body = "x-list-name: #{list.email}\nX-ADD-KEY:"
     mail.add_file('spec/fixtures/example_key.txt')
     mail.deliver
 
@@ -724,7 +810,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-fetch-KEY: lala!"
+    mail.body = "x-list-name: #{list.email}\nX-fetch-KEY: lala!"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -756,7 +842,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-fetch-KEY: admin at example.org"
+    mail.body = "x-list-name: #{list.email}\nX-fetch-KEY: admin at example.org"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -771,7 +857,7 @@ describe "user sends keyword" do
     raw = Mail::TestMailer.deliveries.first
     message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
 
-    expect(message.to_s).to include("Key 98769E8A1091F36BD88403ECF71A3F8412D83889 was fetched (new key)")
+    expect(message.first_plaintext_part.body.to_s).to match(/This key was fetched \(new key\):\n0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]\n/)
 
     teardown_list_and_mailer(list)
   end
@@ -790,7 +876,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-fetch-KEY: something at localhost"
+    mail.body = "x-list-name: #{list.email}\nX-fetch-KEY: something at localhost"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -824,7 +910,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-fetch-KEY: http://127.0.0.1:9999/keys/example.asc"
+    mail.body = "x-list-name: #{list.email}\nX-fetch-KEY: http://127.0.0.1:9999/keys/example.asc"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -839,7 +925,7 @@ describe "user sends keyword" do
     raw = Mail::TestMailer.deliveries.first
     message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
 
-    expect(message.to_s).to include("Key 98769E8A1091F36BD88403ECF71A3F8412D83889 was fetched (new key)")
+    expect(message.first_plaintext_part.body.to_s).to match(/This key was fetched \(new key\):\n0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]\n/)
 
     teardown_list_and_mailer(list)
   end
@@ -859,7 +945,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     url = "http://127.0.0.1:9999/foo"
-    mail.body = "x-listname: #{list.email}\nX-fetch-KEY: #{url}"
+    mail.body = "x-list-name: #{list.email}\nX-fetch-KEY: #{url}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -893,7 +979,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-fetch-KEY: 0x0000000000000000000000000000000000000000"
+    mail.body = "x-list-name: #{list.email}\nX-fetch-KEY: 0x0000000000000000000000000000000000000000"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -927,7 +1013,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-fetch-KEY: 0x98769E8A1091F36BD88403ECF71A3F8412D83889"
+    mail.body = "x-list-name: #{list.email}\nX-fetch-KEY: 0x98769E8A1091F36BD88403ECF71A3F8412D83889"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -942,7 +1028,7 @@ describe "user sends keyword" do
     raw = Mail::TestMailer.deliveries.first
     message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
 
-    expect(message.to_s).to include("Key 98769E8A1091F36BD88403ECF71A3F8412D83889 was fetched (new key)")
+    expect(message.first_plaintext_part.body.to_s).to match(/This key was fetched \(new key\):\n0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]\n/)
 
     teardown_list_and_mailer(list)
   end
@@ -961,7 +1047,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-fetch-KEY: 0x59C71FB38AEE22E091C78259D06350440F759BD3"
+    mail.body = "x-list-name: #{list.email}\nX-fetch-KEY: 0x59C71FB38AEE22E091C78259D06350440F759BD3"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -976,7 +1062,7 @@ describe "user sends keyword" do
     raw = Mail::TestMailer.deliveries.first
     message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
 
-    expect(message.first_plaintext_part.body.to_s).to eql("Key 59C71FB38AEE22E091C78259D06350440F759BD3 was fetched (unchanged).")
+    expect(message.first_plaintext_part.body.to_s).to match(/This key was fetched \(unchanged\):\n0x59C71FB38AEE22E091C78259D06350440F759BD3 schleuder at example.org \d{4}-\d{2}-\d{2}/)
 
     teardown_list_and_mailer(list)
   end
@@ -996,7 +1082,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     content_body = "Hello again!\n"
-    mail.body = "x-listname: #{list.email}\nX-resend: someone at example.org\n#{content_body}"
+    mail.body = "x-list-name: #{list.email}\nX-resend: someone at example.org\n#{content_body}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1021,6 +1107,128 @@ describe "user sends keyword" do
     teardown_list_and_mailer(list)
   end
 
+  it "x-resend does not include internal_footer" do
+    list = create(
+      :list,
+      internal_footer: "-- \nsomething private",
+      public_footer: "-- \nsomething public"
+    )
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.email
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.email => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    content_body = "Hello again!\n"
+    mail.body = "x-list-name: #{list.email}\nX-resend: someone at example.org\n#{content_body}"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.email)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    resent_message = raw.verify
+    resent_message_body = resent_message.parts.map { |p| p.body.to_s }.join
+
+    expect(resent_message_body).not_to include(list.internal_footer)
+    expect(resent_message_body).to eql(content_body + list.public_footer.to_s)
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-resend with iso-8859-1 body" do
+    list = create(:list, public_footer: "-- \nblablabla")
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.email
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.email => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    content_body = "Hello again! ¡Hola!\n"
+    mail.charset = 'iso-8859-1'
+    mail.body = "x-list-name: #{list.email}\nX-resend: someone at example.org\n#{content_body}".encode('iso-8859-1')
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.email)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    resent_message = raw.verify
+    raw = Mail::TestMailer.deliveries.last
+    message = Mail.create_message_to_list(raw.to_s, list.email, list).setup
+
+    expect(message.to).to eql(['schleuder at example.org'])
+    expect(message.to_s).to include("Resent: Unencrypted to someone at example.org")
+    expect(message.parts[1].body.to_s.force_encoding(message.parts[1].charset)).to eql(content_body.encode(message.parts[1].charset))
+    expect(resent_message.to).to include("someone at example.org")
+    expect(resent_message.to_s).not_to include("Resent: Unencrypted to someone at example.org")
+    expect(resent_message.parts[0].body.to_s).to eql(content_body.encode(resent_message.parts[0].charset))
+    expect(resent_message.parts[1].body.to_s).to eql(list.public_footer.to_s)
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-resend with utf-8 body and umlauts" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.email
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.email => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    content_body = "This is a test\nAnd here are some umlauts:ÄäÖöÜüß"
+    mail.charset = 'utf-8'
+    mail.body = "x-list-name: #{list.email}\nX-resend: someone at example.org\n#{content_body}".encode('utf-8')
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.email)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    resent_message = raw.verify
+    raw = Mail::TestMailer.deliveries.last
+    message = Mail.create_message_to_list(raw.to_s, list.email, list).setup
+
+    expect(message.to).to eql(['schleuder at example.org'])
+    expect(message.to_s).to include("Resent: Unencrypted to someone at example.org")
+    expect(message.parts[1].body.to_s.force_encoding(message.parts[1].charset)).to eql(content_body.encode(message.parts[1].charset))
+    expect(resent_message.to).to include("someone at example.org")
+    expect(resent_message.to_s).not_to include("Resent: Unencrypted to someone at example.org")
+    expect(resent_message.parts[0].body.to_s).to eql(content_body.encode(resent_message.parts[0].charset))
+
+    teardown_list_and_mailer(list)
+  end
+
   it "x-resend with admin-notification" do
     list = create(:list, keywords_admin_notify: ['resend'])
     list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
@@ -1036,7 +1244,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     content_body = "Hello again!\n"
-    mail.body = "x-listname: #{list.email}\nX-resend: someone at example.org\n#{content_body}"
+    mail.body = "x-list-name: #{list.email}\nX-resend: someone at example.org\n#{content_body}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1062,7 +1270,7 @@ describe "user sends keyword" do
     teardown_list_and_mailer(list)
   end
 
-  it "x-resend without x-listname" do
+  it "x-resend without x-list-name" do
     list = create(:list)
     list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
     ENV['GNUPGHOME'] = list.listdir
@@ -1092,7 +1300,7 @@ describe "user sends keyword" do
 
     expect(message.to).to eql(['schleuder at example.org'])
     expect(message.to_s).not_to include("Resent: Unencrypted to someone at example.org")
-    expect(message.to_s).to include("Your message didn't contain the mandatory X-LISTNAME-keyword, thus it was rejected.")
+    expect(message.to_s).to include("Your message didn't contain the mandatory X-LIST-NAME-keyword, thus it was rejected.")
 
     teardown_list_and_mailer(list)
   end
@@ -1114,7 +1322,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     content_body = "Hello again!\n"
-    mail.body = "x-listname: #{list.email}\nX-resend-encrypted-only: bla at foo\n#{content_body}"
+    mail.body = "x-list-name: #{list.email}\nX-resend-encrypted-only: bla at foo\n#{content_body}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1136,6 +1344,45 @@ describe "user sends keyword" do
     teardown_list_and_mailer(list)
   end
 
+  it "x-resend-unencrypted with matching key" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    ENV['GNUPGHOME'] = list.listdir
+    list.import_key(File.read("spec/fixtures/bla_foo_key.txt"))
+    mail = Mail.new
+    mail.to = list.email
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.email => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    content_body = "Hello again!\n"
+    mail.body = "x-list-name: #{list.email}\nX-resend-unencrypted: bla at foo\n#{content_body}"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.email)
+    rescue SystemExit
+    end
+    resent_message = Mail::TestMailer.deliveries.first
+    raw = Mail::TestMailer.deliveries.last
+    message = Mail.create_message_to_list(raw.to_s, list.email, list).setup
+
+    expect(list.keys('bla at foo').size).to eql(1)
+    expect(resent_message.to).to eql(['bla at foo'])
+    expect(resent_message.content_type).to_not match(/^multipart\/encrypted.*application\/pgp-encrypted/)
+    expect(resent_message.first_plaintext_part.body.to_s).to include('Hello again!')
+    expect(message.first_plaintext_part.body.to_s).to include("Resent: Unencrypted to bla at foo")
+
+    teardown_list_and_mailer(list)
+  end
+
   it "x-resend with expired key" do
     list = create(:list)
     list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
@@ -1152,7 +1399,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     content_body = "Hello again!\n"
-    mail.body = "x-listname: #{list.email}\nX-resend-encrypted-only: bla at foo\n#{content_body}"
+    mail.body = "x-list-name: #{list.email}\nX-resend-encrypted-only: bla at foo\n#{content_body}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1171,7 +1418,7 @@ describe "user sends keyword" do
     teardown_list_and_mailer(list)
   end
 
-  it "x-resend with wrong x-listname" do
+  it "x-resend with wrong x-list-name" do
     list = create(:list)
     list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
     ENV['GNUPGHOME'] = list.listdir
@@ -1186,7 +1433,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     content_body = "Hello again!\n"
-    mail.body = "x-listname: somethingelse at example.org\nX-resend: someone at example.org\n#{content_body}"
+    mail.body = "x-list-name: somethingelse at example.org\nX-resend: someone at example.org\n#{content_body}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1201,7 +1448,44 @@ describe "user sends keyword" do
 
     expect(message.to).to eql(['schleuder at example.org'])
     expect(message.to_s).not_to include("Resent: Unencrypted to someone at example.org")
-    expect(message.to_s).to include("Your message contained a wrong X-LISTNAME-keyword. The value of that keyword must match the email address of this list.")
+    expect(message.to_s).to include("Your message contained a wrong X-LIST-NAME-keyword. The value of that keyword must match the email address of this list.")
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-resend with invalid recipient" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.email
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.email => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    content_body = "Hello again!\n"
+    invalid_recipient = '`ls`bla'
+    mail.body = "x-list-name: #{list.email}\nX-resend: #{invalid_recipient}\n#{content_body}"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.email)
+    rescue SystemExit
+    end
+    delivered_emails = Mail::TestMailer.deliveries
+    raw = delivered_emails.first
+    message = Mail.create_message_to_list(raw.to_s, list.email, list).setup
+
+    expect(delivered_emails.size).to eql(1)
+    expect(message.to_s).not_to include("Resent: Unencrypted to someone at example.org")
+    expect(message.to_s).to include("Error: Invalid email-address for resending: #{invalid_recipient}")
 
     teardown_list_and_mailer(list)
   end
@@ -1221,7 +1505,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     signed_text = "signed\nsigned\nsigned\n\n"
-    mail.body = "x-listname: #{list.email}\nx-sign-this:\n#{signed_text}"
+    mail.body = "x-list-name: #{list.email}\nx-sign-this:\n#{signed_text}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1254,7 +1538,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     keywords = Mail::Part.new
-    keywords.body = "\n\nx-listname: #{list.email}\nx-sign-this:"
+    keywords.body = "\n\nx-list-name: #{list.email}\nx-sign-this:"
     mail.parts << keywords
     signed_content = File.read('spec/fixtures/example_key.txt')
     mail.attachments['example_key.txt'] = { mime_type: 'application/pgp-key',
@@ -1301,7 +1585,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-list-KEYs: der at ex"
+    mail.body = "x-list-name: #{list.email}\nX-list-KEYs: der at ex"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1315,7 +1599,7 @@ describe "user sends keyword" do
     message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
 
     expect(message.to).to eql(['schleuder at example.org'])
-    expect(message.to_s).to include("pub   4096R/59C71FB38AEE22E091C78259D06350440F759BD3 2016-12-06")
+    expect(message.to_s).to match(/pub   4096R\/59C71FB38AEE22E091C78259D06350440F759BD3 \d{4}-\d{2}-\d{2}/)
     expect(message.to_s.scan(/^pub /).size).to eql(1)
 
     teardown_list_and_mailer(list)
@@ -1335,7 +1619,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-list-KEYs: @schleuder"
+    mail.body = "x-list-name: #{list.email}\nX-list-KEYs: @schleuder"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1349,7 +1633,7 @@ describe "user sends keyword" do
     message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
 
     expect(message.to).to eql(['schleuder at example.org'])
-    expect(message.to_s).to include("pub   4096R/59C71FB38AEE22E091C78259D06350440F759BD3 2016-12-06")
+    expect(message.to_s).to match(/pub   4096R\/59C71FB38AEE22E091C78259D06350440F759BD3 \d{4}-\d{2}-\d{2}/)
     expect(message.to_s.scan(/^pub /).size).to eql(1)
 
     teardown_list_and_mailer(list)
@@ -1369,7 +1653,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-list-KEYs: 0x59C71FB38AEE22E091C78259D06350440F759BD3"
+    mail.body = "x-list-name: #{list.email}\nX-list-KEYs: 0x59C71FB38AEE22E091C78259D06350440F759BD3"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1383,7 +1667,7 @@ describe "user sends keyword" do
     message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
 
     expect(message.to).to eql(['schleuder at example.org'])
-    expect(message.to_s).to include("pub   4096R/59C71FB38AEE22E091C78259D06350440F759BD3 2016-12-06")
+    expect(message.to_s).to match(/pub   4096R\/59C71FB38AEE22E091C78259D06350440F759BD3 \d{4}-\d{2}-\d{2}/)
     expect(message.to_s.scan(/^pub /).size).to eql(1)
 
     teardown_list_and_mailer(list)
@@ -1404,7 +1688,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-GET-KEY: 0x59C71FB38AEE22E091C78259D06350440F759BD3"
+    mail.body = "x-list-name: #{list.email}\nX-GET-KEY: 0x59C71FB38AEE22E091C78259D06350440F759BD3"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1418,7 +1702,7 @@ describe "user sends keyword" do
     message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
 
     expect(message.to).to eql(['schleuder at example.org'])
-    expect(message.to_s).to include("pub   4096R/59C71FB38AEE22E091C78259D06350440F759BD3 2016-12-06")
+    expect(message.to_s).to match(/pub   4096R\/59C71FB38AEE22E091C78259D06350440F759BD3 \d{4}-\d{2}-\d{2}/)
     expect(message.to_s).to include("-----BEGIN PGP PUBLIC KEY")
 
     teardown_list_and_mailer(list)
@@ -1438,7 +1722,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-get-KEY: blabla"
+    mail.body = "x-list-name: #{list.email}\nX-get-KEY: blabla"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1472,7 +1756,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-get-KEY:"
+    mail.body = "x-list-name: #{list.email}\nX-get-KEY:"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1507,7 +1791,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-delete-KEY: C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
+    mail.body = "x-list-name: #{list.email}\nX-delete-KEY: C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1541,7 +1825,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-delete-KEY: lala"
+    mail.body = "x-list-name: #{list.email}\nX-delete-KEY: lala"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1576,7 +1860,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-delete-KEY: schleuder"
+    mail.body = "x-list-name: #{list.email}\nX-delete-KEY: schleuder"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1611,7 +1895,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-get-logfile"
+    mail.body = "x-list-name: #{list.email}\nX-get-logfile"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1647,7 +1931,7 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
-    mail.body = "x-listname: #{list.email}\nX-get-logfile"
+    mail.body = "x-list-name: #{list.email}\nX-get-logfile"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1682,7 +1966,7 @@ describe "user sends keyword" do
     }
     mail.gpg(gpg_opts)
     content_body = "something something list-key"
-    mail.body = "x-listname: #{list.email}\nX-attach-listkey\n#{content_body}"
+    mail.body = "x-list-name: #{list.email}\nX-attach-listkey\n#{content_body}"
     mail.deliver
 
     encrypted_mail = Mail::TestMailer.deliveries.first
@@ -1699,7 +1983,7 @@ describe "user sends keyword" do
     expect(message.parts.last.parts.length).to eql(2)
     expect(message.parts.last.parts.first.body.to_s).to eql(content_body)
     expect(message.parts.last.parts.last.content_type.to_s).to eql("application/pgp-keys")
-    expect(message.parts.last.parts.last.body.decoded).to include("pub   4096R/59C71FB38AEE22E091C78259D06350440F759BD3 2016-12-06")
+    expect(message.parts.last.parts.last.body.decoded).to match(/pub   4096R\/59C71FB38AEE22E091C78259D06350440F759BD3 \d{4}-\d{2}-\d{2}/)
     expect(message.parts.last.parts.last.body.decoded).to include("-----BEGIN PGP PUBLIC KEY BLOCK-----")
     expect(message.parts.last.parts.last.body.decoded).to include("mQINBFhGvz0BEADXbbTWo/PStyTznAo/f1UobY0EiVPNKNERvYua2Pnq8BwOQ5bS")
 
@@ -1720,6 +2004,40 @@ describe "user sends keyword" do
       sign_as: list.admins.first.fingerprint
     }
     mail.gpg(gpg_opts)
+    mail.body = "x-list-name: #{list.email}\nX-get-version"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.request_address)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
+
+    expect(message.to).to eql(['schleuder at example.org'])
+    expect(message.first_plaintext_part.body.to_s.lines.size).to eql(1)
+    expect(message.first_plaintext_part.body.to_s).to eql(Schleuder::VERSION)
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-get-version with depreciated x-listname keyword" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.request_address
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.request_address => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
     mail.body = "x-listname: #{list.email}\nX-get-version"
     mail.deliver
 
@@ -1739,5 +2057,116 @@ describe "user sends keyword" do
 
     teardown_list_and_mailer(list)
   end
+  
+  it "x-list-keys without arguments" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    list.import_key(File.read('spec/fixtures/example_key.txt'))
+    list.import_key(File.read('spec/fixtures/bla_foo_key.txt'))
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.request_address
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.request_address => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    mail.body = "x-list-name: #{list.email}\nX-list-keys"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.request_address)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
+
+    expect(message.first_plaintext_part.body.to_s.lines.size).to eql(16)
+    expect(message.first_plaintext_part.body.to_s).to include("59C71FB38AEE22E091C78259D06350440F759BD3")
+    expect(message.first_plaintext_part.body.to_s).to include("C4D60F8833789C7CAA44496FD3FFA6613AB10ECE")
+    expect(message.first_plaintext_part.body.to_s).to include("87E65ED2081AE3D16BE4F0A5EBDBE899251F2412")
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-list-keys with one argument" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    list.import_key(File.read('spec/fixtures/example_key.txt'))
+    list.import_key(File.read('spec/fixtures/bla_foo_key.txt'))
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.request_address
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.request_address => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    mail.body = "x-list-name: #{list.email}\nX-list-keys schleuder2"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.request_address)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
+
+    expect(message.first_plaintext_part.body.to_s.lines.size).to eql(4)
+    expect(message.first_plaintext_part.body.to_s).not_to include("59C71FB38AEE22E091C78259D06350440F759BD3")
+    expect(message.first_plaintext_part.body.to_s).to include("C4D60F8833789C7CAA44496FD3FFA6613AB10ECE")
+    expect(message.first_plaintext_part.body.to_s).not_to include("87E65ED2081AE3D16BE4F0A5EBDBE899251F2412")
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-list-keys with two arguments" do
+    list = create(:list)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    list.import_key(File.read('spec/fixtures/example_key.txt'))
+    list.import_key(File.read('spec/fixtures/bla_foo_key.txt'))
+    ENV['GNUPGHOME'] = list.listdir
+    mail = Mail.new
+    mail.to = list.request_address
+    mail.from = list.admins.first.email
+    gpg_opts = {
+      encrypt: true,
+      keys: {list.request_address => list.fingerprint},
+      sign: true,
+      sign_as: list.admins.first.fingerprint
+    }
+    mail.gpg(gpg_opts)
+    mail.body = "x-list-name: #{list.email}\nX-list-keys schleuder2 bla"
+    mail.deliver
+
+    encrypted_mail = Mail::TestMailer.deliveries.first
+    Mail::TestMailer.deliveries.clear
+
+    begin
+      Schleuder::Runner.new().run(encrypted_mail.to_s, list.request_address)
+    rescue SystemExit
+    end
+    raw = Mail::TestMailer.deliveries.first
+    message = Mail.create_message_to_list(raw.to_s, list.request_address, list).setup
+
+    expect(message.first_plaintext_part.body.to_s.lines.size).to eql(10)
+    expect(message.first_plaintext_part.body.to_s).not_to include("59C71FB38AEE22E091C78259D06350440F759BD3")
+    expect(message.first_plaintext_part.body.to_s).to include("C4D60F8833789C7CAA44496FD3FFA6613AB10ECE")
+    expect(message.first_plaintext_part.body.to_s).to include("87E65ED2081AE3D16BE4F0A5EBDBE899251F2412")
+
+    teardown_list_and_mailer(list)
+  end
 
 end
diff --git a/spec/schleuder/runner_spec.rb b/spec/schleuder/runner_spec.rb
index 63c92c3..9f8fee6 100644
--- a/spec/schleuder/runner_spec.rb
+++ b/spec/schleuder/runner_spec.rb
@@ -126,6 +126,42 @@ describe Schleuder::Runner do
 
         teardown_list_and_mailer(list)
       end
+
+      it "includes the internal_footer" do
+        list = create(
+          :list, 
+          send_encrypted_only: false,
+          internal_footer: "-- \nfor our eyes only!"
+        )
+        list.subscribe("admin at example.org", nil, true)
+        mail = File.read("spec/fixtures/mails/plain/thunderbird.eml")
+
+        Schleuder::Runner.new().run(mail, list.email)
+        message = Mail::TestMailer.deliveries.first
+
+        expect(message.parts.first.parts.last.body.to_s).to eql(list.internal_footer)
+
+        teardown_list_and_mailer(list)
+      end
+      
+      it "does not include the public_footer" do
+        public_footer = "-- \nsomething public blabla"
+        list = create(
+          :list, 
+          send_encrypted_only: false,
+          internal_footer: "-- \nfor our eyes only!",
+          public_footer: public_footer
+        )
+        list.subscribe("admin at example.org", nil, true)
+        mail = File.read("spec/fixtures/mails/plain/thunderbird.eml")
+
+        Schleuder::Runner.new().run(mail, list.email)
+        message = Mail::TestMailer.deliveries.first
+
+        expect(message.parts.first.to_s).not_to include(list.public_footer)
+
+        teardown_list_and_mailer(list)
+      end
     end
 
     it "delivers a signed error message if a subscription's key is expired on a encrypted-only list" do
@@ -205,6 +241,53 @@ describe Schleuder::Runner do
       teardown_list_and_mailer(list)
     end
 
+    context "Quoted-Printable encoding" do
+      it "is handled properly in cleartext emails" do
+        list = create(:list, send_encrypted_only: false)
+        list.subscribe("admin at example.org", nil, true)
+        mail = File.read('spec/fixtures/mails/qp-encoding-clear.eml')
+
+        Schleuder::Runner.new().run(mail, list.email)
+        message = Mail::TestMailer.deliveries.first
+        content_part = message.parts.first
+
+        expect(content_part.parts.last.content_transfer_encoding).to eql('quoted-printable')
+        expect(content_part.parts.last.body.encoded).to include('=3D86')
+        expect(content_part.parts.last.body.encoded).not_to include('=86')
+
+        teardown_list_and_mailer(list)
+      end
+
+      it "is handled properly in encrypted+signed emails" do
+        list = create(:list, send_encrypted_only: false)
+        list.subscribe("admin at example.org", "59C71FB38AEE22E091C78259D06350440F759BD3", true)
+        mail = File.read('spec/fixtures/mails/qp-encoding-encrypted+signed.eml')
+
+        Schleuder::Runner.new().run(mail, list.email)
+        raw = Mail::TestMailer.deliveries.first
+        message = Mail.create_message_to_list(raw.to_s, list.email, list).setup
+        content_part = message.parts.last.first_plaintext_part
+
+        expect(content_part.decoded).to include('bug=86')
 
+        teardown_list_and_mailer(list)
+      end
+
+      it "is handled properly in encrypted emails" do
+        list = create(:list, send_encrypted_only: false)
+        list.subscribe("admin at example.org", nil, true)
+        mail = File.read('spec/fixtures/mails/qp-encoding-encrypted.eml')
+
+        Schleuder::Runner.new().run(mail, list.email)
+        message = Mail::TestMailer.deliveries.first
+        content_part = message.parts.first
+
+        expect(content_part.parts.last.content_transfer_encoding).to eql('quoted-printable')
+        expect(content_part.parts.last.body.encoded).to include('=3D86')
+        expect(content_part.parts.last.body.encoded).not_to include('=86')
+
+        teardown_list_and_mailer(list)
+      end
+    end
   end
 end
diff --git a/spec/schleuder/unit/conf_spec.rb b/spec/schleuder/unit/conf_spec.rb
new file mode 100644
index 0000000..d032767
--- /dev/null
+++ b/spec/schleuder/unit/conf_spec.rb
@@ -0,0 +1,38 @@
+require "spec_helper"
+
+describe Schleuder::Conf do
+  it "reads ERB code in config files" do
+    # Suppress warnings about already defined constants
+    # if using "load" further below
+    verbose_orig = $VERBOSE
+    $VERBOSE = nil
+    
+    # Define constants
+    val_old = "val_old"
+
+    # Check if env var is set
+    if not ENV["SCHLEUDER_DB_PATH"].nil?
+      val_old = ENV["SCHLEUDER_DB_PATH"]
+    end
+   
+    # Set env var, reload the config and check whether the correct value
+    # is returned
+    val_test = "SCHLEUDER_ERB_TEST"
+    ENV["SCHLEUDER_DB_PATH"] = val_test
+    load "schleuder/conf.rb" 
+    expect(Schleuder::Conf.database["database"]).to eql(val_test)
+
+    # Reset the env var
+    ENV["SCHLEUDER_DB_PATH"] = nil
+
+    # Set the env var to the original value
+    if val_old != "val_old"
+      ENV["SCHLEUDER_DB_PATH"] = val_old
+    end
+    
+    load "schleuder/conf.rb"
+
+    # Set verbose level to original value
+    $VERBOSE = $verbose_orig
+  end
+end
diff --git a/spec/schleuder/unit/filters_spec.rb b/spec/schleuder/unit/filters_spec.rb
index 2eef9b9..4d40e09 100644
--- a/spec/schleuder/unit/filters_spec.rb
+++ b/spec/schleuder/unit/filters_spec.rb
@@ -2,7 +2,7 @@ require "spec_helper"
 
 describe Schleuder::Filters do
 
-  context '.fix_hostmail_messages!' do
+  context '.fix_hotmail_messages!' do
     it "fixes pgp/mime-messages that were mangled by hotmail" do
       message = Mail.read("spec/fixtures/mails/hotmail.eml")
       Schleuder::Filters.fix_hotmail_messages!(nil, message)
diff --git a/spec/schleuder/unit/gpgme_ctx.rb b/spec/schleuder/unit/gpgme_ctx.rb
index 24a14d0..06648f4 100644
--- a/spec/schleuder/unit/gpgme_ctx.rb
+++ b/spec/schleuder/unit/gpgme_ctx.rb
@@ -168,4 +168,26 @@ describe GPGME::Ctx do
     expect(out.class).to eql(NilClass)
     expect(exitcode.class).to eql(Integer)
   end
+
+  context "#keyserver_arg" do
+    it "returns keyserver-args if a keyserver is configured" do
+      list = create(:list)
+
+      keyserver_args = list.gpg.send(:keyserver_arg)
+
+      expect(keyserver_args).to eql("--keyserver #{Conf.keyserver}")
+    end
+
+    it "returns a blank string if the keyserver-option is set to a blank value" do
+      oldval = Conf.instance.config['keyserver']
+      Conf.instance.config['keyserver'] = ''
+      list = create(:list)
+
+      keyserver_args = list.gpg.send(:keyserver_arg)
+
+      expect(keyserver_args).to eql("")
+
+      Conf.instance.config['keyserver'] = oldval
+    end
+  end
 end
diff --git a/spec/schleuder/unit/gpgme_key.rb b/spec/schleuder/unit/gpgme_key.rb
new file mode 100644
index 0000000..245c132
--- /dev/null
+++ b/spec/schleuder/unit/gpgme_key.rb
@@ -0,0 +1,50 @@
+require "spec_helper"
+
+describe GPGME::Key do
+  describe "#oneline" do
+    it "displays the expected basic attributes" do
+      list = create(:list)
+
+      key = list.key
+
+      expect(key.oneline).to match(/0x59C71FB38AEE22E091C78259D06350440F759BD3 schleuder at example.org \d{4}-\d{2}-\d{2}/)
+    end
+      
+    it "displays the expected attributes for an expiring key" do
+      list = create(:list)
+      list.import_key(File.read("spec/fixtures/expiring_key.txt"))
+
+      key = list.key("421FBF7190640136788593CD9EE9BE5929CACC20")
+
+      expect(key.oneline).to match(/0x421FBF7190640136788593CD9EE9BE5929CACC20 expiringkey at example.org \d{4}-\d{2}-\d{2} \[expires: \d{4}-\d{2}-\d{2}\]/)
+    end
+
+    it "displays the expected attributes for an expired key" do
+      list = create(:list)
+      list.import_key(File.read("spec/fixtures/expired_key.txt"))
+
+      key = list.key("98769E8A1091F36BD88403ECF71A3F8412D83889")
+      
+      expect(key.oneline).to match(/0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]/)
+    end
+
+    it "displays the expected attributes for a revoked key" do
+      list = create(:list)
+      list.import_key(File.read("spec/fixtures/revoked_key.txt"))
+
+      key = list.key("7E783CDE6D1EFE6D2409739C098AC83A4C0028E9")
+      
+      expect(key.oneline).to match(/0x7E783CDE6D1EFE6D2409739C098AC83A4C0028E9 paz at nadir.org \d{4}-\d{2}-\d{2} \[revoked\]/)
+    end
+
+    # gpgme.rb doesn't report missing encryption-capability properly yet.
+    it "displays the expected attributes for a key that's not capable of encryption" do
+      list = create(:list)
+      list.import_key(File.read("spec/fixtures/signonly_key.txt"))
+
+      key = list.key("B1CD8BB15C2673C6BFD8FA4B70B2CF29E01AD53E")
+      
+      expect(key.oneline).to match(/0xB1CD8BB15C2673C6BFD8FA4B70B2CF29E01AD53E signonly at example.org \d{4}-\d{2}-\d{2} \[not capable of encryption\]/)
+    end
+  end
+end
diff --git a/spec/schleuder/unit/list_builder_spec.rb b/spec/schleuder/unit/list_builder_spec.rb
index 40d7040..f715a6d 100644
--- a/spec/schleuder/unit/list_builder_spec.rb
+++ b/spec/schleuder/unit/list_builder_spec.rb
@@ -6,7 +6,7 @@ describe Schleuder::ListBuilder do
     listname = "list-#{rand}@example.org"
     adminaddress = 'schleuder2 at example.org'
     adminkey = File.read('spec/fixtures/example_key.txt')
-    list, messages = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, adminkey).run
+    list, messages = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, nil, adminkey).run
     expect(list).to be_an_instance_of Schleuder::List
     expect(list).to be_valid
     expect(messages).to be_blank
@@ -16,7 +16,7 @@ describe Schleuder::ListBuilder do
     listname = "list-#{rand}"
     adminaddress = 'schleuder2 at example.org'
     adminkey = File.read('spec/fixtures/example_key.txt')
-    list, messages = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, adminkey).run
+    list, messages = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, nil, adminkey).run
     expect(list).to be_nil
     expect(messages).to be_an_instance_of Hash
     expect(messages.keys).to eq ['email']
@@ -27,7 +27,7 @@ describe Schleuder::ListBuilder do
     listname = "list-#{rand}@example.org"
     adminaddress = 'schleuder2 at example.org'
     adminkey = File.read('spec/fixtures/example_key.txt')
-    list, _ = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, adminkey).run
+    list, _ = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, nil, adminkey).run
     expect(File.directory?(list.listdir)).to be true
   end
 
@@ -35,7 +35,7 @@ describe Schleuder::ListBuilder do
     listname = "list-#{rand}@example.org"
     adminaddress = 'schleuder2 at example.org'
     adminkey = File.read('spec/fixtures/example_key.txt')
-    list, _ = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, adminkey).run
+    list, _ = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, nil, adminkey).run
     uids = list.key.uids.map(&:email)
     expect(uids).to include(list.email)
     expect(uids).to include(list.request_address)
@@ -46,10 +46,38 @@ describe Schleuder::ListBuilder do
     listname = "list-#{rand}@example.org"
     adminaddress = 'schleuder2 at example.org'
     adminkey = File.read('spec/fixtures/example_key.txt')
-    list, _ = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, adminkey).run
+    list, _ = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, nil, adminkey).run
+
+    subscription_emails = list.subscriptions.map(&:email)
+    keys_fingerprints = list.keys.map(&:fingerprint)
+
+    expect(subscription_emails).to eq [adminaddress]
+    expect(keys_fingerprints).to include("C4D60F8833789C7CAA44496FD3FFA6613AB10ECE")
+  end
+
+  it "subscribes the adminaddress and respects the given adminfingerprint" do
+    listname = "list-#{rand}@example.org"
+    adminaddress = 'schleuder2 at example.org'
+    list, _ = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, "59C71FB38AEE22E091C78259D06350440F759BD3").run
+
+    subscription_emails = list.subscriptions.map(&:email)
+    admin_subscription = list.admins.first
+
+    expect(subscription_emails).to eq [adminaddress]
+    expect(admin_subscription.fingerprint).to eql("59C71FB38AEE22E091C78259D06350440F759BD3")
+  end
+
+  it "subscribes the adminaddress and ignores the adminfingerprint if an adminkey was given" do
+    listname = "list-#{rand}@example.org"
+    adminaddress = 'schleuder2 at example.org'
+    adminkey = File.read('spec/fixtures/example_key.txt')
+    list, _ = ListBuilder.new({email: listname, fingerprint: nil}, adminaddress, "59C71FB38AEE22E091C78259D06350440F759BD3", adminkey).run
+
     subscription_emails = list.subscriptions.map(&:email)
-    keys_emails = list.keys.map(&:uids).flatten.map(&:email)
+    subscription_fingerprints = list.subscriptions.map(&:fingerprint)
+
     expect(subscription_emails).to eq [adminaddress]
-    expect(keys_emails).to include(adminaddress)
+    expect(subscription_fingerprints).to eq ["C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"]
   end
+
 end
diff --git a/spec/schleuder/unit/list_spec.rb b/spec/schleuder/unit/list_spec.rb
index 61d3875..ea23063 100644
--- a/spec/schleuder/unit/list_spec.rb
+++ b/spec/schleuder/unit/list_spec.rb
@@ -24,6 +24,7 @@ describe Schleuder::List do
   it { is_expected.to respond_to :subject_prefix_in }
   it { is_expected.to respond_to :subject_prefix_out }
   it { is_expected.to respond_to :openpgp_header_preference }
+  it { is_expected.to respond_to :internal_footer }
   it { is_expected.to respond_to :public_footer }
   it { is_expected.to respond_to :headers_to_meta }
   it { is_expected.to respond_to :bounces_drop_on_headers }
@@ -185,7 +186,14 @@ describe Schleuder::List do
     expect(list.errors.messages[:language]).to include("must be one of: en, de")
   end
 
-  it "is invalid if public footer include a non-printable characters" do
+  it "is invalid if internal_footer includes a non-printable character" do
+    list = build(:list, internal_footer: "\a")
+
+    expect(list).not_to be_valid
+    expect(list.errors.messages[:internal_footer]).to include("includes non-printable characters")
+  end
+
+  it "is invalid if public_footer includes a non-printable character" do
     list = build(:list, public_footer: "\a")
 
     expect(list).not_to be_valid
@@ -197,7 +205,7 @@ describe Schleuder::List do
       expect(Schleuder::List.configurable_attributes).to eq [
        :bounces_drop_all, :bounces_drop_on_headers, :bounces_notify_admins,
        :forward_all_incoming_to_admins, :headers_to_meta, :include_list_headers,
-       :include_openpgp_header, :keep_msgid, :keywords_admin_notify, :keywords_admin_only,
+       :include_openpgp_header, :internal_footer, :keep_msgid, :keywords_admin_notify, :keywords_admin_only,
        :language, :log_level, :logfiles_to_keep, :max_message_size_kb, :openpgp_header_preference,
        :public_footer, :receive_admin_only, :receive_authenticated_only, :receive_encrypted_only,
        :receive_from_subscribed_emailaddresses_only, :receive_signed_only, :send_encrypted_only,
@@ -349,6 +357,8 @@ describe Schleuder::List do
     it "exports the key with the fingerprint of the list if no argument is given" do
       list = create(:list, email: "schleuder at example.org")
       expected_public_key = File.read("spec/fixtures/schleuder_at_example_public_key.txt")
+      # Get rid of the first, opening line, so we don't compare against optional comments in the output, etc.
+      expected_public_key = expected_public_key.split("\n").slice(1..-1).join("\n")
 
       expect(list.export_key()).to include expected_public_key
     end
@@ -357,6 +367,8 @@ describe Schleuder::List do
   it "exports the key with the given fingerprint" do
     list = create(:list, email: "schleuder at example.org")
     expected_public_key = File.read("spec/fixtures/schleuder_at_example_public_key.txt")
+    # Get rid of the first, opening line, so we don't compare against optional comments in the output, etc.
+    expected_public_key = expected_public_key.split("\n").slice(1..-1).join("\n")
 
     expect(
       list.export_key("59C71FB38AEE22E091C78259D06350440F759BD3")
@@ -367,33 +379,43 @@ describe Schleuder::List do
     it "adds a mesage if a key expires in two weeks or less" do
       list = create(:list)
       key = double("key")
+      generation_time = Time.now - 1.year
+      expiry_time = Time.now + 7.days
       allow_any_instance_of(GPGME::Key).to receive(:subkeys).and_return(key)
       allow(key).to receive(:first).and_return(key)
-      allow(key).to receive(:expires).and_return(Time.now + 7.days)
+      allow(key).to receive(:timestamp).and_return(generation_time)
+      allow(key).to receive(:expires?).and_return(true)
+      allow(key).to receive(:expired?).and_return(false)
+      allow(key).to receive(:expired).and_return(false)
+      allow(key).to receive(:any?).and_return(false)
+      allow(key).to receive(:expires).and_return(expiry_time)
       allow(key).to receive(:fingerprint).and_return("59C71FB38AEE22E091C78259D06350440F759BD3")
 
-      expect(list.check_keys).to eq "Key 59C71FB38AEE22E091C78259D06350440F759BD3 expires in 6 days.\n"
+      datefmt = "%Y-%m-%d"
+      generation_date = generation_time.strftime(datefmt)
+      expiry_date = expiry_time.strftime(datefmt)
+      expect(list.check_keys).to eq "This key expires in 6 days:\n0x59C71FB38AEE22E091C78259D06350440F759BD3 schleuder at example.org #{generation_date} [expires: #{expiry_date}]\n\n"
     end
 
     it "adds a message if a key is revoked" do
       list = create(:list)
       allow_any_instance_of(GPGME::Key).to receive(:trust).and_return(:revoked)
 
-      expect(list.check_keys).to eq "Key 59C71FB38AEE22E091C78259D06350440F759BD3 is revoked.\n"
+      expect(list.check_keys).to match(/This key is revoked:\n0x59C71FB38AEE22E091C78259D06350440F759BD3 schleuder at example.org \d{4}-\d{2}-\d{2} \[revoked\]\n\n/)
     end
 
     it "adds a message if a key is disabled" do
       list = create(:list)
       allow_any_instance_of(GPGME::Key).to receive(:trust).and_return(:disabled)
 
-      expect(list.check_keys).to eq "Key 59C71FB38AEE22E091C78259D06350440F759BD3 is disabled.\n"
+      expect(list.check_keys).to match(/This key is disabled:\n0x59C71FB38AEE22E091C78259D06350440F759BD3 schleuder at example.org \d{4}-\d{2}-\d{2} \[disabled\]\n\n/)
     end
 
     it "adds a message if a key is invalid" do
       list = create(:list)
       allow_any_instance_of(GPGME::Key).to receive(:trust).and_return(:invalid)
 
-      expect(list.check_keys).to eq "Key 59C71FB38AEE22E091C78259D06350440F759BD3 is invalid.\n"
+      expect(list.check_keys).to match(/This key is invalid:\n0x59C71FB38AEE22E091C78259D06350440F759BD3 schleuder at example.org \d{4}-\d{2}-\d{2} \[invalid\]\n\n/)
     end
   end
 
@@ -463,7 +485,7 @@ describe Schleuder::List do
         output = list.fetch_keys('98769E8A1091F36BD88403ECF71A3F8412D83889')
       end
 
-      expect(output).to include("98769E8A1091F36BD88403ECF71A3F8412D83889 was fetched (new key)")
+      expect(output).to match(/This key was fetched \(new key\):\n0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]/)
 
       teardown_list_and_mailer(list)
     end
@@ -477,7 +499,7 @@ describe Schleuder::List do
         output = list.fetch_keys('http://127.0.0.1:9999/keys/example.asc')
       end
 
-      expect(output).to include("98769E8A1091F36BD88403ECF71A3F8412D83889 was fetched (new key)")
+      expect(output).to match(/This key was fetched \(new key\):\n0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]/)
 
       teardown_list_and_mailer(list)
     end
@@ -491,7 +513,7 @@ describe Schleuder::List do
         output = list.fetch_keys('admin at example.org')
       end
 
-      expect(output).to include("98769E8A1091F36BD88403ECF71A3F8412D83889 was fetched (new key)")
+      expect(output).to match(/This key was fetched \(new key\):\n0x98769E8A1091F36BD88403ECF71A3F8412D83889 bla at foo \d{4}-\d{2}-\d{2} \[expired: \d{4}-\d{2}-\d{2}\]/)
 
       teardown_list_and_mailer(list)
     end
@@ -510,4 +532,180 @@ describe Schleuder::List do
       expect(raw.parts.first.parts.last.body.to_s).to include("-----BEGIN PGP PUBLIC KEY BLOCK-----")
     end
   end
+
+  describe "#subscribe" do
+    it "subscribes and ignores nil-values for admin and delivery_enabled" do
+      list = create(:list)
+      sub, _ = list.subscribe("admin at example.org", nil, nil, nil)
+
+      expect(sub.admin?).to be(false)
+      expect(sub.delivery_enabled?).to be(true)
+    end
+
+    it "subscribes and sets the fingerprint from key material that contains exactly one key" do
+      list = create(:list)
+      key_material = File.read("spec/fixtures/example_key.txt")
+      sub, msgs = list.subscribe("admin at example.org", "", true, true, key_material)
+
+      expect(msgs).to be(nil)
+      expect(sub.fingerprint).to eql("C4D60F8833789C7CAA44496FD3FFA6613AB10ECE")
+      expect(list.subscriptions.size).to be(1)
+      expect(list.subscriptions.first.fingerprint).to eql("C4D60F8833789C7CAA44496FD3FFA6613AB10ECE")
+      expect(list.keys.size).to be(2)
+      expect(list.keys.map(&:fingerprint)).to eql(["59C71FB38AEE22E091C78259D06350440F759BD3", "C4D60F8833789C7CAA44496FD3FFA6613AB10ECE"])
+    end
+
+    it "subscribes and does not set the fingerprint from key material containing multiple keys" do
+      list = create(:list)
+      key_material = File.read("spec/fixtures/example_key.txt")
+      key_material << File.read("spec/fixtures/olduid_key.txt")
+      sub, msgs = list.subscribe("admin at example.org", "", true, true, key_material)
+
+      expect(msgs).to eql("The given key material contained more than one key, could not determine which fingerprint to use. Please set it manually!")
+      expect(sub.fingerprint).to be_blank
+      expect(list.subscriptions.size).to be(1)
+      expect(list.subscriptions.first.fingerprint).to be_blank
+      expect(list.keys.size).to be(3)
+      expect(list.keys.map(&:fingerprint)).to eql(["59C71FB38AEE22E091C78259D06350440F759BD3", "C4D60F8833789C7CAA44496FD3FFA6613AB10ECE", "6EE51D78FD0B33DE65CCF69D2104E20E20889F66"])
+    end
+
+    it "subscribes and does not set the fingerprint from key material containing no keys" do
+      list = create(:list)
+      key_material = "blabla"
+      sub, msgs = list.subscribe("admin at example.org", "", true, true, key_material)
+
+      expect(msgs).to eql("The given key material did not contain any keys!")
+      expect(sub.fingerprint).to be_blank
+      expect(list.subscriptions.size).to be(1)
+      expect(list.subscriptions.first.fingerprint).to be_blank
+      expect(list.keys.size).to be(1)
+      expect(list.keys.map(&:fingerprint)).to eql(["59C71FB38AEE22E091C78259D06350440F759BD3"])
+    end
+
+    it "subscribes and ignores a given fingerprint if key material is given, too" do
+      list = create(:list)
+      key_material = "blabla"
+      sub, msgs = list.subscribe("admin at example.org", "C4D60F8833789C7CAA44496FD3FFA6613AB10ECE", true, true, key_material)
+
+      expect(msgs).to eql("The given key material did not contain any keys!")
+      expect(sub.fingerprint).to be_blank
+      expect(list.subscriptions.size).to be(1)
+      expect(list.subscriptions.first.fingerprint).to be_blank
+      expect(list.keys.size).to be(1)
+      expect(list.keys.map(&:fingerprint)).to eql(["59C71FB38AEE22E091C78259D06350440F759BD3"])
+    end
+  end
+
+  describe "#send_to_subscriptions" do
+    it "sends the message to all subscribers" do
+      list = create(:list, send_encrypted_only: false)
+      sub, msgs = list.subscribe("admin at example.org", nil, true)
+      sub, msgs = list.subscribe("user1 at example.org")
+      sub, msgs = list.subscribe("user2 at example.org")
+      mail = Mail.new
+      mail.to = list.email
+      mail.from = 'something at localhost'
+      mail.subject = 'Something'
+      mail.body = "Some content"
+
+      Schleuder::Runner.new().run(mail, list.email)
+      messages = Mail::TestMailer.deliveries
+      recipients = messages.map { |m| m.to.first }.sort
+
+      expect(messages.size).to be(3)
+      expect(recipients).to eql(['admin at example.org', 'user1 at example.org', 'user2 at example.org'])
+      expect(messages[0].parts.first.parts.last.body.to_s).to eql("Some content")
+      expect(messages[0].subject).to eql("Something")
+      expect(messages[1].parts.first.parts.last.body.to_s).to eql("Some content")
+      expect(messages[1].subject).to eql("Something")
+      expect(messages[2].parts.first.parts.last.body.to_s).to eql("Some content")
+      expect(messages[2].subject).to eql("Something")
+
+      teardown_list_and_mailer(list)
+    end
+
+    it "sends the message to all subscribers, in the clear if one's key is unusable, if send_encrypted_only is false" do
+      list = create(:list, send_encrypted_only: false)
+      sub, msgs = list.subscribe("admin at example.org", nil, true)
+      key_material = File.read("spec/fixtures/expired_key.txt")
+      sub, msgs = list.subscribe("user1 at example.org", nil, false, true, key_material)
+      sub, msgs = list.subscribe("user2 at example.org")
+      mail = Mail.new
+      mail.to = list.email
+      mail.from = 'something at localhost'
+      mail.subject = 'Something'
+      mail.body = "Some content"
+
+      Schleuder::Runner.new().run(mail, list.email)
+      messages = Mail::TestMailer.deliveries
+      recipients = messages.map { |m| m.to.first }.sort
+
+      expect(messages.size).to be(3)
+      expect(recipients).to eql(['admin at example.org', 'user1 at example.org', 'user2 at example.org'])
+      expect(messages[0].parts.first.parts.last.body.to_s).to eql("Some content")
+      expect(messages[0].subject).to eql("Something")
+      expect(messages[1].parts.first.parts.last.body.to_s).to eql("Some content")
+      expect(messages[1].subject).to eql("Something")
+      expect(messages[2].parts.first.parts.last.body.to_s).to eql("Some content")
+      expect(messages[2].subject).to eql("Something")
+
+      teardown_list_and_mailer(list)
+    end
+
+    it "sends the message only to subscribers with available keys if send_encrypted_only is true, and a notification to the other subscribers" do
+      list = create(:list, send_encrypted_only: true)
+      sub, msgs = list.subscribe("admin at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+      sub, msgs = list.subscribe("user1 at example.org")
+      sub, msgs = list.subscribe("user2 at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3')
+      mail = Mail.new
+      mail.to = list.email
+      mail.from = 'something at localhost'
+      mail.subject = 'Something'
+      mail.body = "Some content"
+
+      Schleuder::Runner.new().run(mail, list.email)
+      messages = Mail::TestMailer.deliveries
+      recipients = messages.map { |m| m.to.first }.sort
+
+      expect(messages.size).to be(3)
+      expect(recipients).to eql(['admin at example.org', 'user1 at example.org', 'user2 at example.org'])
+      expect(messages[0].parts.last.body.to_s).to include("-----BEGIN PGP MESSAGE-----")
+      expect(messages[0].subject).to eql("Something")
+      expect(messages[1].parts.first.body.to_s).to include("You missed an email")
+      expect(messages[1].subject).to eql("Notice")
+      expect(messages[2].parts.last.body.to_s).to include("-----BEGIN PGP MESSAGE-----")
+      expect(messages[2].subject).to eql("Something")
+
+      teardown_list_and_mailer(list)
+    end
+
+    it "sends the message only to subscribers with usable keys if send_encrypted_only is true, and a notification to the other subscribers" do
+      list = create(:list, send_encrypted_only: true)
+      key_material = File.read("spec/fixtures/partially_expired_key.txt")
+      sub, msgs = list.subscribe("admin at example.org", nil, true, true, key_material)
+      key_material = File.read("spec/fixtures/expired_key.txt")
+      sub, msgs = list.subscribe("user1 at example.org", nil, false, true, key_material)
+      sub, msgs = list.subscribe("user2 at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3')
+      mail = Mail.new
+      mail.to = list.email
+      mail.from = 'something at localhost'
+      mail.subject = 'Something'
+      mail.body = "Some content"
+
+      Schleuder::Runner.new().run(mail, list.email)
+      messages = Mail::TestMailer.deliveries
+      recipients = messages.map { |m| m.to.first }.sort
+
+      expect(messages.size).to be(3)
+      expect(recipients).to eql(['admin at example.org', 'user1 at example.org', 'user2 at example.org'])
+      expect(messages[0].parts.first.body.to_s).to include("You missed an email")
+      expect(messages[0].subject).to eql("Notice")
+      expect(messages[1].parts.first.body.to_s).to include("You missed an email")
+      expect(messages[1].subject).to eql("Notice")
+      expect(messages[2].parts.last.body.to_s).to include("-----BEGIN PGP MESSAGE-----")
+      expect(messages[2].subject).to eql("Something")
+
+      teardown_list_and_mailer(list)
+    end
+  end
 end
diff --git a/spec/schleuder/unit/logger_notifications_spec.rb b/spec/schleuder/unit/logger_notifications_spec.rb
index 3fe5186..5a77821 100644
--- a/spec/schleuder/unit/logger_notifications_spec.rb
+++ b/spec/schleuder/unit/logger_notifications_spec.rb
@@ -81,4 +81,33 @@ describe Schleuder::LoggerNotifications do
     expect(message.parts.first.parts[2].body.to_s).to include("Subject: A subject")
     expect(message.parts.first.parts[2][:content_type].content_type).to eql("message/rfc822")
   end
+
+  it "notifies admins encryptedly if their key is usable" do
+    list = create(:list, send_encrypted_only: false)
+    list.subscribe("schleuder at example.org", '59C71FB38AEE22E091C78259D06350440F759BD3', true)
+    mail = Mail.new
+    mail.subject = "A subject"
+    list.logger.notify_admin(["Something", "anotherthing"], mail.to_s, I18n.t('notice'))
+
+    message = Mail::TestMailer.deliveries.first
+
+    expect(message.subject).to eql('Notice')
+    expect(message.parts.size).to be(2)
+    expect(message.parts.last.body.to_s).to include('-----BEGIN PGP MESSAGE-----')
+  end
+
+  it "notifies admins in the clear if their key is unusable" do
+    list = create(:list, send_encrypted_only: false)
+    key_material = File.read("spec/fixtures/partially_expired_key.txt")
+    list.subscribe("schleuder at example.org", nil, true, true, key_material)
+    mail = Mail.new
+    mail.subject = "A subject"
+    list.logger.notify_admin("Something", mail.to_s, I18n.t('notice'))
+
+    message = Mail::TestMailer.deliveries.first
+
+    expect(message.subject).to eql('Notice')
+    expect(message.parts.size).to be(2)
+    expect(message.parts.first.parts.first.body.to_s).to eql('Something')
+  end
 end
diff --git a/spec/schleuder/unit/message_spec.rb b/spec/schleuder/unit/message_spec.rb
index be3c2f5..72926ed 100644
--- a/spec/schleuder/unit/message_spec.rb
+++ b/spec/schleuder/unit/message_spec.rb
@@ -100,5 +100,29 @@ describe Mail::Message do
       expect(message.subject).to eql('Re: [prefix] test')
     end
   end
+
+  it "adds list#public_footer as last mime-part without changing its value" do
+    footer = "\n\n-- \nblabla\n  blabla\n  "
+    list = create(:list)
+    list.public_footer = footer
+    mail = Mail.new
+    mail.body = 'blabla'
+    mail.list = list
+    mail.add_public_footer!
+
+    expect(mail.parts.last.body.to_s).to eql(footer)
+  end
+
+  it "adds list#internal_footer as last mime-part without changing its value" do
+    footer = "\n\n-- \nblabla\n  blabla\n  "
+    list = create(:list)
+    list.internal_footer = footer
+    mail = Mail.new
+    mail.body = 'blabla'
+    mail.list = list
+    mail.add_internal_footer!
+
+    expect(mail.parts.last.body.to_s).to eql(footer)
+  end
 end
 
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6348995..51f3d7f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,12 +1,24 @@
 ENV['SCHLEUDER_ENV'] ||= 'test'
 ENV['SCHLEUDER_CONFIG'] = 'spec/schleuder.yml'
 ENV["SCHLEUDER_LIST_DEFAULTS"] = "etc/list-defaults.yml"
-require 'bundler/setup'
-Bundler.setup
+if ENV['USE_BUNDLER'] != 'false'
+  require 'bundler/setup'
+  Bundler.setup
+end
+# We need to do this before requiring any other code
+# Check env if we want to run code coverage analysis
+if ENV['CHECK_CODE_COVERAGE'] != 'false'
+  require 'simplecov'
+  require 'simplecov-console'
+  SimpleCov::Formatter::Console.table_options = {max_width: 400}
+  SimpleCov.formatter = SimpleCov::Formatter::Console
+  SimpleCov.start
+end
 require 'schleuder'
 require 'schleuder/cli'
 require 'database_cleaner'
 require 'factory_girl'
+require 'net/http'
 
 RSpec.configure do |config|
   config.expect_with :rspec do |expectations|
@@ -34,6 +46,8 @@ RSpec.configure do |config|
 
   config.after(:each) do |example|
     FileUtils.rm_rf(Dir["spec/gnupg/pubring.gpg~"])
+    `gpgconf --kill dirmngr > /dev/null 2>&1`
+    `gpgconf --kill gpg-agent > /dev/null 2>&1`
   end
 
   config.after(:suite) do
@@ -62,8 +76,22 @@ RSpec.configure do |config|
 
   def with_sks_mock
     pid = Process.spawn('spec/sks-mock.rb', [:out, :err] => ["/tmp/sks-mock.log", 'w'])
-    sleep 1
+    uri = URI.parse("http://127.0.0.1:9999/status")
+    attempts = 5
+    begin
+      sleep 1
+      Net::HTTP.get(uri)
+    rescue Errno::ECONNREFUSED => exc
+      attempts -= 1
+      if attempts > 0
+        retry
+      else
+        raise "sks-mock.rb failed to start, cannot continue: #{exc}"
+      end
+    end
+
     yield
+
     Process.kill 'TERM', pid
     Process.wait pid
   end

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/schleuder.git



More information about the Pkg-ruby-extras-commits mailing list