[DRE-commits] [schleuder] 42/52: New upstream version 3.0.0

Georg Faerber georg-alioth-guest at moszumanska.debian.org
Mon Feb 6 11:21:22 UTC 2017


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

georg-alioth-guest pushed a commit to branch master
in repository schleuder.

commit 390fce646050e2419b043564a49a8a7d99715fc9
Author: Georg Faerber <georg at riseup.net>
Date:   Thu Jan 26 00:14:39 2017 +0100

    New upstream version 3.0.0
---
 .travis.yml                                      |   2 +-
 CHANGELOG.md                                     |  38 +++-
 Gemfile.lock                                     |   2 +-
 README.md                                        |  16 +-
 Rakefile                                         |   2 +-
 bin/pinentry-clearpassphrase                     |  25 +--
 bin/schleuder                                    |   4 +
 bin/schleuder-api-daemon                         |  56 ++---
 etc/postfix/schleuder_sqlite.cf                  |  28 +++
 etc/schleuder.yml                                |  12 +-
 lib/schleuder/cli.rb                             |  20 +-
 lib/schleuder/cli/cert.rb                        |   1 -
 lib/schleuder/conf.rb                            |  12 +-
 lib/schleuder/errors/keyword_admin_only.rb       |   4 +-
 lib/schleuder/gpgme/ctx.rb                       |  92 +++++++-
 lib/schleuder/gpgme/key.rb                       | 107 ++++++---
 lib/schleuder/list.rb                            |  40 ++--
 lib/schleuder/list_builder.rb                    |   8 +
 lib/schleuder/listlogger.rb                      |   3 +-
 lib/schleuder/logger_notifications.rb            |  10 +-
 lib/schleuder/mail/message.rb                    |  15 +-
 lib/schleuder/plugins/key_management.rb          |  55 ++++-
 lib/schleuder/plugins/subscription_management.rb |  26 ++-
 lib/schleuder/plugins_runner.rb                  |  32 ++-
 lib/schleuder/runner.rb                          |   4 +-
 lib/schleuder/subscription.rb                    |  23 +-
 lib/schleuder/version.rb                         |   2 +-
 locales/de.yml                                   |  30 ++-
 locales/en.yml                                   |  28 ++-
 man/schleuder-api-daemon.8                       |  27 +--
 man/schleuder-api-daemon.8.ron                   |  14 +-
 man/schleuder.8                                  |  12 +-
 man/schleuder.8.ron                              |  10 +-
 spec/fixtures/expired_key.txt                    |  29 +++
 spec/fixtures/expired_key_extended.txt           |  29 +++
 spec/schleuder.yml                               |   1 +
 spec/schleuder/integration/cli_spec.rb           |  39 ++++
 spec/schleuder/integration/keywords_spec.rb      | 269 +++++++++++++++++++++++
 spec/schleuder/runner_spec.rb                    |  37 +++-
 spec/schleuder/unit/list_spec.rb                 |   2 +-
 spec/schleuder/unit/subscription_spec.rb         |   2 +-
 spec/sks-mock.rb                                 |  26 +++
 spec/spec_helper.rb                              |  14 ++
 43 files changed, 981 insertions(+), 227 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 86690b9..1fbbd69 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,7 +3,7 @@ rvm:
   - 2.1
   - 2.2
   - 2.3
-  - 2.4
+  - 2.4.0-rc1
 before_install:
   - gem install bundler
   - sudo apt-get -qq update
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d44de8..d3107e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,42 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 The format of this file is based on [Keep a Changelog](http://keepachangelog.com/).
 
+## [3.0.0] / 2017-01-26
+
+### Changed
+
+* **API-keys always required!** From now on all requests to schleuder-api-daemon require API-keys, even via localhost. This helps protecting against rogue non-root-accounts or -scripts on the local machine.
+* **TLS always used!** schleuder-api-daemon now always uses TLS.
+* Switched project-site and git-repository to <https://0xacab.org/schleuder/schleuder>.
+* Set proper usage flags when creating a new OpenPGP-key: the primary key gets "SC", the subkey "E". (Thanks, dkg!)
+* Avoid possible future errors by ignoring every unknown output of gpg (like GnuPG's doc/DETAILS recommends). (Thanks, dkg!)
+* Friendlier error message if delivery to subscription fails.
+* Set list-email as primary address after adding UIDs. Previously it was a little random, for reasons only known to GnuPG.
+* Only use temporary files where neccessary, and with more secure paths.
+* Tighten requirements for valid email-addresses a little: The domain-part may now only contain alpha-numeric characters, plus these: `._-`
+* Required version of schleuder-cli: 0.0.2.
+
+### Added
+
+* X-LISTNAME: A **new mandatory keyword** to accompany all keywords. From now on every message containing keywords must also include the listname-keyword like this: `X-LISTNAME: list at hostname`
+
+  The other keywords will only be run if the given listname matches the email-address of the list that the message is sent to. This mitigates replay-attacks among different lists.
+* Also send helpful message if a subscription's key is present but unusable.
+* Provide simpler postfix integration, now using virtual_domains and an sql-script. (Thanks, dkg!)
+* Enable refreshing keys from keyservers: A script that is meant to be run regularly from cron. It refreshes each key of each list one by one from a configurable keyserver, and sends the result to the respective list-admins.
+* Import attached, ascii-armored keys from messages with `add-key`-keyword. (Thanks, Kéfir!)
+* Check possible key-material for expected format before importing it. (Thanks, Kéfir!)
+
+### Fixed
+
+* Allow fingerprints to be prefixed with '0x' in `subscribe`-keyword.
+* Also delete directory of list-logfile on deletion if that resides outside of the list-dir.
+* Sign and possibly encrypt error notifications.
+* Fix setting admin- and delivery-flags while subscribing.
+* Fix subscribing from schleuder-cli.
+* Fix finding subscriptions from signatures made by a signing-capable sub-key.
+
+
 ## [3.0.0.beta17] / 2017-01-12
 
 ### Changed
@@ -12,7 +48,7 @@ The format of this file is based on [Keep a Changelog](http://keepachangelog.com
 * Stopped using SCHLEUDER_ROOT in specs. Those make life difficult for packaging for debian.
 * While running specs, ensure smtp-daemon.rb has been stopped before starting it anew.
 
-###
+### Added
 
 * A Code of Conduct.
 
diff --git a/Gemfile.lock b/Gemfile.lock
index dc92336..0efdbb4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,7 +1,7 @@
 PATH
   remote: .
   specs:
-    schleuder (3.0.0.beta17)
+    schleuder (3.0.0)
       activerecord (~> 4.1)
       mail-gpg (~> 0.3.0)
       rake (~> 10)
diff --git a/README.md b/README.md
index 6dec597..878ff89 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Schleuder, version 3
 Schleuder is a gpg-enabled mailing list manager with resending-capabilities. Subscribers can communicate encrypted (and pseudonymously) among themselves, receive emails from non-subscribers and send emails to non-subscribers via the list.
 
 Version 3 of schleuder is a complete rewrite, which aims to be more robust, flexible, and internationalized. It
-also provides an API for the optional web interface called [schleuder-web](https://git.codecoop.org/schleuder/schleuder-web).
+also provides an API for the optional web interface called [schleuder-web](https://0xacab.org/schleuder/schleuder-web).
 
 For more details see <https://schleuder.nadir.org/docs/>.
 
@@ -42,15 +42,15 @@ Additionally these **rubygems** are required (will be installed automatically un
 Installing Schleuder
 ------------
 
-1. Download [the gem](https://git.codecoop.org/schleuder/schleuder3/raw/master/gems/schleuder-3.0.0.beta17.gem) and [the OpenPGP-signature](https://git.codecoop.org/schleuder/schleuder3/raw/master/gems/schleuder-3.0.0.beta17.gem.sig) and verify:
+1. Download [the gem](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.0.0.gem) and [the OpenPGP-signature](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.0.0.gem.sig) and verify:
    ```
    gpg --recv-key 0xB3D190D5235C74E1907EACFE898F2C91E2E6E1F3
-   gpg --verify schleuder-3.0.0.beta17.gem.sig
+   gpg --verify schleuder-3.0.0.gem.sig
    ```
 
 2. If all went well install the gem:
    ```
-   gem install schleuder-3.0.0.beta17.gem
+   gem install schleuder-3.0.0.gem
    ```
 
 3. Set up schleuder:
@@ -81,11 +81,11 @@ List administration
 -------------------
 
 Please use
-[schleuder-cli](https://git.codecoop.org/schleuder/schleuder-cli) to create and
+[schleuder-cli](https://0xacab.org/schleuder/schleuder-cli) to create and
 manage lists from the command line.
 
 Optionally consider installing
-[schleuder-web](https://git.codecoop.org/schleuder/schleuder-web), the web
+[schleuder-web](https://0xacab.org/schleuder/schleuder-web), the web
 interface for schleuder. It enables list-admins to manage their lists through
 the web instead of using [request-keywords](https://schleuder.nadir.org/docs/#subscription-and-key-management).
 
@@ -94,7 +94,7 @@ the web instead of using [request-keywords](https://schleuder.nadir.org/docs/#su
 Todo
 ----
 
-See <https://git.codecoop.org/schleuder/schleuder3/issues>.
+See <https://0xacab.org/schleuder/schleuder/issues>.
 
 Testing
 -------
@@ -130,4 +130,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://git.codecoop.org/schleuder/schleuder3/raw/master/gems/schleuder-3.0.0.beta17.tar.gz) and [its OpenPGP-signature](https://git.codecoop.org/schleuder/schleuder3/raw/master/gems/schleuder-3.0.0.beta17.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.0.0.tar.gz) and [its OpenPGP-signature](https://0xacab.org/schleuder/schleuder/raw/master/gems/schleuder-3.0.0.tar.gz.sig).
diff --git a/Rakefile b/Rakefile
index 0564fd5..d5f61a0 100644
--- a/Rakefile
+++ b/Rakefile
@@ -124,7 +124,7 @@ end
 desc 'Check if version-tag already exists'
 task :check_version do
   # Check if Schleuder::VERSION has been updated since last release
-  if `git tag`.include?(@tagname)
+  if `git tag`.match?(/^#{@tagname}$/)
     $stderr.puts "Warning: Tag '#{@tagname}' already exists. Did you forget to update #{project}/version.rb?"
     $stderr.print "Delete tag to continue? [yN] "
     if $stdin.gets.match(/^y/i)
diff --git a/bin/pinentry-clearpassphrase b/bin/pinentry-clearpassphrase
index f171aae..7eef8cf 100755
--- a/bin/pinentry-clearpassphrase
+++ b/bin/pinentry-clearpassphrase
@@ -3,11 +3,10 @@
 # This file can be deleted once we cease to support gnupg 2.0.
 
 require 'fileutils'
-require 'openssl'
 require 'cgi'
+require 'openssl'
 
 def respond(msg, flush=true)
-  log '>', msg
   $stdout.puts msg
   if flush
     $stdout.flush
@@ -35,44 +34,30 @@ end
 
 def do_exit
   if File.exist?(EMPTYPWDSENTFILE2)
-    log "deleting tmpdir"
     FileUtils.rm_rf(TMPDIR)
   end
-  log "exiting\n"
   exit 0
 end
 
-def log(*args)
-  if DEBUG
-    File.open('/tmp/pinentry.log', 'a') do |f|
-      f.puts "#{args.join(' ')}"
-    end
-  end
-end
-
-
 OLDPASSWD = CGI.escape(ENV['PINENTRY_USER_DATA'].to_s)
 if OLDPASSWD.empty?
   respond "Fatal error: passed PINENTRY_USER_DATA was empty, cannot continue"
   exit 1
 end
 
-DEBUG = false
-SALT = 'Thank you, Edward!'
-TMPNAME = Digest::SHA256.hexdigest(SALT + ENV['GNUPGHOME'].to_s)
-TMPDIR = "/tmp/schleuder-#{TMPNAME}"
-OLDPWDSENTFILE = File.join(TMPDIR, "1")
+# We need a static directory name to maintain the state across invocations of
+# this file.
+TMPDIR = File.join(ENV['GNUPGHOME'], '.tmp-pinentry-clearpassphrase')
+OLDPWDSENTFILE = File.join(TMPDIR, '1')
 EMPTYPWDSENTFILE1 = File.join(TMPDIR, '2')
 EMPTYPWDSENTFILE2 = File.join(TMPDIR, '3')
 if ! Dir.exist?(TMPDIR)
   Dir.mkdir(TMPDIR)
 end
 
-log "\nnew connection at #{Time.now}"
 respond "OK - what's up?"
 
 while line = $stdin.gets do
-  log '<', line
   case line
   when /^GETPIN/
     send_password
diff --git a/bin/schleuder b/bin/schleuder
index 9cfaaed..d7062a8 100755
--- a/bin/schleuder
+++ b/bin/schleuder
@@ -1,5 +1,9 @@
 #!/usr/bin/env ruby
 
+# Don't emit any warnings, they would be sent back to the email-sender as an
+# error-message.
+$VERBOSE=nil
+
 trap("INT") { exit 1 }
 
 
diff --git a/bin/schleuder-api-daemon b/bin/schleuder-api-daemon
index 4044c6d..497ceb2 100755
--- a/bin/schleuder-api-daemon
+++ b/bin/schleuder-api-daemon
@@ -9,11 +9,21 @@ require 'sinatra/namespace'
 require 'thin'
 require_relative '../lib/schleuder.rb'
 
-TLS_CERT = Conf.api['tls_cert_file']
-TLS_KEY = Conf.api['tls_key_file']
+
+%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
@@ -25,28 +35,6 @@ class SchleuderApiDaemon < Sinatra::Base
     end
   end
 
-  # TODO: Move this into a method, call that in the configure block
-  @use_tls = Conf.api_use_tls?
-  if @use_tls
-    [TLS_CERT, TLS_KEY].each do |const|
-      if const.nil?
-        @use_tls = false
-      elsif ! File.readable?(const)
-        $stderr.puts "Error: '#{const}' is not a readable file."
-        exit 1
-      end
-    end
-  end
-
-  if @use_tls
-    use Rack::Auth::Basic, "Schleuder API Daemon" do |username, key|
-      username == 'schleuder' && Conf.api_valid_api_keys.include?(key)
-    end
-  else
-    $stderr.puts "\nWarning: Without TLS, schleuder-api-daemon enforces binding to localhost only!\nExecute `schleuder cert generate` and follow the instructions.\n\n"
-    set :bind, 'localhost'
-  end
-
   before do
     cast_param_values
   end
@@ -164,7 +152,7 @@ class SchleuderApiDaemon < Sinatra::Base
         email: key.email,
         ascii: key.armored,
         expiry: key.expires,
-        trust_issues: key.trust
+        trust_issues: key.usability_issue
       }
     end
   end
@@ -256,11 +244,13 @@ class SchleuderApiDaemon < Sinatra::Base
     post '.json' do
       begin
         list = list(requested_list_id)
+        adminflag = [true, 1, '1'].include?(parsed_body['admin'])
+        deliveryflag = [true, 1, '1'].include?(parsed_body['delivery_enabled'])
         sub = list.subscribe(
             parsed_body['email'],
             parsed_body['fingerprint'],
-            parsed_body['admin'],
-            parsed_body['delivery_enabled']
+            adminflag,
+            deliveryflag
           )
         logger.debug "subcription: #{sub.inspect}"
         if sub.valid?
@@ -354,13 +344,11 @@ class SchleuderApiDaemon < Sinatra::Base
 
   def self.run!
     super do |server|
-      if @use_tls == true
-        server.ssl = true
-        server.ssl_options = {
-          :cert_chain_file  => TLS_CERT,
-          :private_key_file => TLS_KEY
-        }
-      end
+      server.ssl = true
+      server.ssl_options = {
+        :cert_chain_file  => Conf.api['tls_cert_file'],
+        :private_key_file => Conf.api['tls_key_file']
+      }
     end
   end
 
diff --git a/etc/postfix/schleuder_sqlite.cf b/etc/postfix/schleuder_sqlite.cf
new file mode 100644
index 0000000..b252843
--- /dev/null
+++ b/etc/postfix/schleuder_sqlite.cf
@@ -0,0 +1,28 @@
+# Use this as a table for postfix to select addresses that schleuder
+# thinks belong to it.  This is useful when
+# smtpd_reject_unlisted_recipient = yes (which is the default for
+# modern Postfix)
+
+# For example, you might dedicate Postfix's "virtual" domains to
+# schleuder with the following set of configs in main.cf:
+#
+# virtual_domains = lists.example.org
+# virtual_transport = schleuder
+# virtual_alias_maps = hash:/etc/postfix/virtual_aliases
+# virtual_mailbox_maps = sqlite:/etc/postfix/schleuder_sqlite.cf
+# schleuder_destination_recipient_limit = 1
+
+# it is not recommended to use this table for more powerful
+# configuration options (e.g. transport_maps) because it could give
+# the schleuder user (which can write the given sqlite database) the
+# power to change settings for for other mail handled by this Postfix
+# instance.
+
+dbpath = /var/lib/schleuder/db.sqlite
+
+query = select 'present' where '%s' in (
+  select email from lists union
+  select replace(email, '@', '-bounces@') from lists union
+  select replace(email, '@', '-owner@') from lists union
+  select replace(email, '@', '-request@') from lists union
+  select replace(email, '@', '-sendkey@') from lists)
diff --git a/etc/schleuder.yml b/etc/schleuder.yml
index 9b152d2..ddf9be9 100644
--- a/etc/schleuder.yml
+++ b/etc/schleuder.yml
@@ -10,6 +10,15 @@ plugins_dir: /etc/schleuder/plugins
 # How verbose should Schleuder log to syslog? (list-specific messages are written to the list's log-file).
 log_level: warn
 
+# Which keyserver to refresh keys from (used by `schleuder refresh_keys`, meant
+# to be run from cron weekly).
+# If you have gnupg 2.1, we strongly suggest to use a hkps-keyserver:
+#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
+# The default works for all supported versions of gnupg:
+keyserver: pool.sks-keyservers.net
+
 # For these options see documentation for ActionMailer::smtp_settings, e.g. <http://api.rubyonrails.org/classes/ActionMailer/Base.html>.
 smtp_settings:
   address: localhost
@@ -28,12 +37,9 @@ database:
     database: /var/lib/schleuder/db.sqlite
     timeout: 5000
 
-# Note: The API-daemon will bind only to localhost if no TLS-cert+keys are available.
 api:
   host: localhost
   port: 4443
-  # Serve the API via https?
-  use_tls: false
   # Certificate and key to use. You can create new ones with `schleuder cert generate`.
   tls_cert_file: /etc/schleuder/schleuder-certificate.pem
   tls_key_file: /etc/schleuder/schleuder-private-key.pem
diff --git a/lib/schleuder/cli.rb b/lib/schleuder/cli.rb
index 947f31b..87622a3 100644
--- a/lib/schleuder/cli.rb
+++ b/lib/schleuder/cli.rb
@@ -63,6 +63,18 @@ module Schleuder
       end
     end
 
+    desc 'refresh_keys', "Refresh all keys of all list from the keyservers sequentially (one by one). (This is supposed to be run from cron weekly.)"
+    def refresh_keys
+      List.all.each do |list|
+        I18n.locale = list.language
+        output = list.refresh_keys
+        if output.present?
+          msg = "#{I18n.t('refresh_keys_intro', email: list.email)}\n\n#{output}"
+          list.logger.notify_admin(msg, nil, I18n.t('refresh_keys'))
+        end
+      end
+    end
+
     desc 'install', "Set-up or update Schleuder environment (create folders, copy files, fill the database)."
     def install
       config_dir = Pathname.new(ENV['SCHLEUDER_CONFIG']).dirname
@@ -97,7 +109,6 @@ module Schleuder
         end
       end
 
-
       if ActiveRecord::SchemaMigration.table_exists?
         say `cd #{root_dir} && rake db:migrate`
       else
@@ -107,6 +118,10 @@ module Schleuder
         end
       end
 
+      if ! File.exist?(Conf.api['tls_cert_file']) || ! File.exist?(Conf.api['tls_key_file'])
+        Schleuder::Cert.new.generate
+      end
+
       say "Schleuder has been set up. You can now create a new list using `schleuder-cli`.\nWe hope you enjoy!"
     rescue => exc
       fatal exc.message
@@ -159,7 +174,7 @@ module Schleuder
 
       # Clear passphrase of imported list-key.
       output = list.key.clearpassphrase(conf['gpg_password'])
-      if output
+      if output.present?
         fatal "while clearing passphrase of list-key: #{output.inspect}"
       end
 
@@ -268,6 +283,7 @@ Please notify the users and admins of this list of these changes.
           KEYWORDS[keyword.downcase]
         end.compact
       end
+
     end
   end
 end
diff --git a/lib/schleuder/cli/cert.rb b/lib/schleuder/cli/cert.rb
index ce5b8ee..9f3689a 100644
--- a/lib/schleuder/cli/cert.rb
+++ b/lib/schleuder/cli/cert.rb
@@ -9,7 +9,6 @@ module Schleuder
       fingerprint = SchleuderCertManager.generate('schleuder', key, cert)
       puts "Fingerprint of generated certificate: #{fingerprint}"
       puts "Have this fingerprint included into the configuration-file of all clients that want to connect to your Schleuder API."
-      puts "To activate TLS set `use_tls: true` in #{ENV['SCHLEUDER_CONFIG']} and restart schleuder-api-daemon."
       if Process.euid == 0
         puts "! Warning: this process was run as root — please make sure the above files are accessible by the user that is running `schleuder-api-daemon`."
       end
diff --git a/lib/schleuder/conf.rb b/lib/schleuder/conf.rb
index 461dd11..dba5674 100644
--- a/lib/schleuder/conf.rb
+++ b/lib/schleuder/conf.rb
@@ -2,13 +2,14 @@ module Schleuder
   class Conf
     include Singleton
 
-    EMAIL_REGEXP = /\A.+ at .+\z/i
+    EMAIL_REGEXP = /\A.+@[[:alnum:]_.-]+\z/i
 
     DEFAULTS = {
       'lists_dir' => '/var/lib/schleuder/lists',
       'listlogs_dir' => '/var/lib/schleuder/lists',
       'plugins_dir' => '/etc/schleuder/plugins',
       'log_level' => 'warn',
+      'keyserver' => 'hkp://pool.sks-keyservers.net',
       'smtp_settings' => {
         'address' => 'localhost',
         'port' => 25,
@@ -31,7 +32,6 @@ module Schleuder
       'api' => {
         'host' => 'localhost',
         'port' => 4443,
-        'use_tls' => false,
         'tls_cert_file' => '/etc/schleuder/schleuder-certificate.pem',
         'tls_key_file' => '/etc/schleuder/schleuder-private-key.pem',
         'valid_api_keys' => []
@@ -74,10 +74,6 @@ module Schleuder
       instance.config['api'] || {}
     end
 
-    def self.api_use_tls?
-      api['use_tls'].to_s == 'true'
-    end
-
     def self.api_valid_api_keys
       Array(api['valid_api_keys'])
     end
@@ -109,6 +105,10 @@ module Schleuder
       settings
     end
 
+    def self.keyserver
+      instance.config['keyserver']
+    end
+
     private
 
     def load_config(filename)
diff --git a/lib/schleuder/errors/keyword_admin_only.rb b/lib/schleuder/errors/keyword_admin_only.rb
index 8d1f362..633d8b1 100644
--- a/lib/schleuder/errors/keyword_admin_only.rb
+++ b/lib/schleuder/errors/keyword_admin_only.rb
@@ -1,12 +1,12 @@
 module Schleuder
   module Errors
-    class KeywordAdminOnly
+    class KeywordAdminOnly < Base
       def initialize(keyword)
         @keyword = keyword
       end
 
       def message
-        t('errors.keyword_admin_only', keyword: keyword)
+        t('errors.keyword_admin_only', keyword: @keyword)
       end
     end
   end
diff --git a/lib/schleuder/gpgme/ctx.rb b/lib/schleuder/gpgme/ctx.rb
index b86d5ee..7f8b69b 100644
--- a/lib/schleuder/gpgme/ctx.rb
+++ b/lib/schleuder/gpgme/ctx.rb
@@ -1,5 +1,12 @@
 module GPGME
   class Ctx
+    IMPORT_FLAGS = {
+      'new key' => 1,
+      'new_uids' => 2,
+      'new_signatures' => 4,
+      'new_subkeys' => 8
+    }
+
     def keyimport(*args)
       self.import_keys(*args)
       result = self.import_result
@@ -35,29 +42,78 @@ module GPGME
       GPGME::Engine.info.find {|e| e.protocol == GPGME::PROTOCOL_OpenPGP }
     end
 
+    def self.refresh_keys(keys)
+      output = []
+      base_args = "--quiet --no-auto-check-trustdb --keyserver #{Conf.keyserver} --refresh-keys"
+      keys.each do |key|
+        args = "#{base_args} #{key.fingerprint}"
+        err, gpgout, _ = gpgcli(args)
+        gpgout = filter_gpgcli_output(gpgout)
+        output << filter_gpgcli_output(err)
+        # Add any gpgkeys-message (gpg 2.0 writes those messages to stdout).
+        # Those could e.g. report a failure to connect to the keyserver.
+        output << gpgout.select { |line| line.match(/^gpgkeys: .*$/) }
+
+        import_stats = translate_import_data(gpgout)
+        if import_stats.present?
+          output << I18n.t("key_updated", { fingerprint: key.fingerprint,
+                                            states: import_stats.join(', ') })
+        end
+        sleep rand(1.0..5.0)
+      end
+      GPGME::Ctx.gpgcli("--check-trustdb")
+      output.flatten.uniq.join
+    end
+
+    def self.translate_import_data(gpgoutput)
+      result = []
+      import_ok = gpgoutput.grep(/IMPORT_OK/).first
+      return result if import_ok.blank?
+
+      import_status = import_ok.split(/\s/).slice(2).to_i
+      return result if import_status.zero?
+
+      # TODO: Raise alarm if new key is found?
+      IMPORT_FLAGS.each do |text, int|
+        if (import_status & int) > 0
+          result << I18n.t("import_states.#{text}")
+        end
+      end
+      result
+    end
+
+    # Unfortunately we can't distinguish between a failure to connect the
+    # keyserver, and a failure to find the key on the server. So we try to
+    # filter misleading errors to check if there are any to be reported.
+    def self.filter_gpgcli_output(strings)
+      strings.reject do |line|
+        line.chomp == 'gpg: keyserver refresh failed: No data' ||
+          line.match(/^gpgkeys: key .* not found on keyserver/) ||
+          line.match(/^gpg: refreshing /) ||
+          line.match(/^gpg: requesting key /) ||
+          line.match(/^gpg: no valid OpenPGP data found/)
+      end
+    end
+
     def self.gpgcli(args)
       exitcode = -1
-      errors = ''
-      output = ''
+      errors = []
+      output = []
       base_cmd = gpg_engine.file_name
-      base_args = "--armor --trust-model always --quiet --no-tty --command-fd 0 --status-fd 1"
+      base_args = "--no-greeting --no-permission-warning --quiet --armor --trust-model always --no-tty --command-fd 0 --status-fd 1"
       cmd = [base_cmd, base_args, args].flatten.join(' ')
       Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
         if block_given?
           output = yield(stdin, stdout, stderr)
+        else
+          output = stdout.readlines
         end
         stdin.close
         errors = stderr.readlines
         exitcode = thread.value.exitstatus
       end
 
-      if output.present?
-        output
-      elsif exitcode > 0
-        errors.join("\n")
-      else
-        nil
-      end
+      [errors, output, exitcode]
     rescue Errno::ENOENT
       raise 'Need gpg in $PATH or in $GPGBIN'
     end
@@ -78,7 +134,21 @@ module GPGME
           end
         end
       end
-      nil
+    end
+
+    def self.spawn_daemon(name, args)
+      delete_daemon_socket(name)
+      cmd = "#{name} #{args} --daemon > /dev/null 2>&1"
+      if ! system(cmd)
+        return [false, "#{name} exited with code #{$?}"]
+      end
+    end
+
+    def self.delete_daemon_socket(name)
+      path = File.join(ENV["GNUPGHOME"], "S.#{name}")
+      if File.exist?(path)
+        File.delete(path)
+      end
     end
   end
 end
diff --git a/lib/schleuder/gpgme/key.rb b/lib/schleuder/gpgme/key.rb
index efba5d9..a4bca4f 100644
--- a/lib/schleuder/gpgme/key.rb
+++ b/lib/schleuder/gpgme/key.rb
@@ -28,6 +28,42 @@ module GPGME
       orig_fingerprint.encode(Encoding::US_ASCII)
     end
 
+    def usable?
+      usability_issue.blank?
+    end
+
+    def usability_issue
+      if trust.present?
+        trust
+      elsif ! usable_for?(:encrypt)
+        "not capable of encryption"
+      else
+        nil
+      end
+    end
+
+    def set_primary_uid(email)
+      # We rely on the order of UIDs here. Seems to work.
+      index = self.uids.map(&:email).index(email)
+      uid_number = index + 1
+      primary_set = false
+      args = "--edit-key '#{self.fingerprint}' #{uid_number}"
+      errors, _ = GPGME::Ctx.gpgcli_expect(args) do |line|
+        case line.chomp
+        when /keyedit.prompt/
+          if ! primary_set
+            primary_set = true
+            "primary"
+          else
+            "save"
+          end
+        else
+          nil
+        end
+      end
+      errors.join
+    end
+
     def adduid(uid, email)
       # This block can be deleted once we cease to support gnupg 2.0.
       if ! GPGME::Ctx.sufficient_gpg_version?('2.1.4')
@@ -35,12 +71,14 @@ module GPGME
       end
 
       # Specifying the key via fingerprint apparently doesn't work.
-      GPGME::Ctx.gpgcli("--quick-adduid #{uid} '#{uid} <#{email}>'")
+      errors, _ = GPGME::Ctx.gpgcli("--quick-adduid #{uid} '#{uid} <#{email}>'")
+      errors.join
     end
 
     # This method can be deleted once we cease to support gnupg 2.0.
     def adduid_expect(uid, email)
-      GPGME::Ctx.gpgcli_expect("--allow-freeform-uid --edit-key '#{self.fingerprint}' adduid") do |line|
+      args = "--allow-freeform-uid --edit-key '#{self.fingerprint}' adduid"
+      errors, _ = GPGME::Ctx.gpgcli_expect(args) do |line|
         case line.chomp
         when /keygen.name/
           uid
@@ -50,12 +88,11 @@ module GPGME
           ''
         when /keyedit.prompt/
           "save"
-        when /USERID_HINT|GOT_IT|GOOD_PASSPHRASE/
-          nil
         else
-          [false, "Unexpected line: #{line}"]
+          nil
         end
       end
+      errors.join
     end
 
     def clearpassphrase(oldpw)
@@ -66,7 +103,8 @@ module GPGME
 
       oldpw_given = false
       # Don't use '--passwd', it claims to fail (even though it factually doesn't).
-      GPGME::Ctx.gpgcli_expect(" --pinentry-mode loopback --edit-key '#{self.fingerprint}' passwd") do |line|
+      args = "--pinentry-mode loopback --edit-key '#{self.fingerprint}' passwd"
+      errors, _, exitcode = GPGME::Ctx.gpgcli_expect(args) do |line|
         case line
         when /passphrase.enter/
           if ! oldpw_given
@@ -81,26 +119,26 @@ module GPGME
           'y'
         when /keyedit.prompt/
           "save"
-        when /USERID_HINT|NEED_PASSPHRASE|GOT_IT|GOOD_PASSPHRASE|MISSING_PASSPHRASE|KEY_CONSIDERED|INQUIRE_MAXLEN|PROGRESS/
-          nil
         else
-          [false, "Unexpected line: #{line}"]
+          nil
         end
       end
+
+      # Only show errors if something apparently went wrong. Otherwise we might
+      # leak useless strings from gpg and make the caller report errors even
+      # though this method succeeded.
+      if exitcode > 0
+        errors.join
+      else
+        nil
+      end
     end
 
     # This method can be deleted once we cease to support gnupg 2.0.
     def clearpassphrase_v20(oldpw)
-      ENV['PINENTRY_USER_DATA'] = oldpw
-      pinentry = File.join(ENV['SCHLEUDER_ROOT'], 'bin', 'pinentry-clearpassphrase')
-      delete_gpg_agent_socket
-      gpg_agent_log = "/tmp/schleuder-gpg-agent-#{rand}.log"
-      gpg_agent_cmd = "gpg-agent --use-standard-socket --pinentry-program #{pinentry} --daemon > #{gpg_agent_log} 2>&1"
-      if ! system(gpg_agent_cmd)
-        return [false, "gpg-agent exited with code #{$?}, output in #{gpg_agent_log}"]
-      end
+      start_gpg_agent(oldpw)
       # Don't use '--passwd', it claims to fail (even though it factually doesn't).
-      output = GPGME::Ctx.gpgcli_expect("--edit-key '#{self.fingerprint}' passwd") do |line|
+      errors, _, exitcode = GPGME::Ctx.gpgcli_expect("--edit-key '#{self.fingerprint}' passwd") do |line|
         case line
         when /BAD_PASSPHRASE/
           [false, 'bad passphrase']
@@ -108,29 +146,32 @@ module GPGME
           'y'
         when /keyedit.prompt/
           "save"
-        when /USERID_HINT|NEED_PASSPHRASE|GOT_IT|GOOD_PASSPHRASE|MISSING_PASSPHRASE|KEY_CONSIDERED|INQUIRE_MAXLEN|PROGRESS/
-          nil
         else
-          [false, "Unexpected line: #{line}"]
+          nil
         end
       end
-      # gpg-agent terminates itself if its socket goes away.
-      delete_gpg_agent_socket
-      delete_file(gpg_agent_log)
-      output
+      stop_gpg_agent
+
+      # Only show errors if something apparently went wrong. Otherwise we might
+      # leak useless strings from gpg and make the caller report errors even
+      # though this method succeeded.
+      if exitcode > 0
+        errors.join
+      else
+        nil
+      end
     end
 
     # This method can be deleted once we cease to support gnupg 2.0.
-    def delete_gpg_agent_socket
-      delete_file(ENV['GNUPGHOME'], 'S.gpg-agent')
+    def stop_gpg_agent
+      # gpg-agent terminates itself if its socket goes away.
+      GPGME::Ctx.delete_daemon_socket('gpg-agent')
     end
 
-    # This method can be deleted once we cease to support gnupg 2.0.
-    def delete_file(*args)
-      path = File.join(Array(args))
-      if File.exist?(path)
-        File.delete(path)
-      end
+    def start_gpg_agent(oldpw)
+      ENV['PINENTRY_USER_DATA'] = oldpw
+      pinentry = File.join(ENV['SCHLEUDER_ROOT'], 'bin', 'pinentry-clearpassphrase')
+      GPGME::Ctx.spawn_daemon('gpg-agent', "--use-standard-socket --pinentry-program #{pinentry}")
     end
   end
 end
diff --git a/lib/schleuder/list.rb b/lib/schleuder/list.rb
index c254d02..6f2e2e3 100644
--- a/lib/schleuder/list.rb
+++ b/lib/schleuder/list.rb
@@ -2,7 +2,7 @@ module Schleuder
   class List < ActiveRecord::Base
 
     has_many :subscriptions, dependent: :destroy
-    before_destroy :delete_listdir
+    before_destroy :delete_listdirs
 
     serialize :headers_to_meta, JSON
     serialize :bounces_drop_on_headers, JSON
@@ -140,8 +140,8 @@ module Schleuder
           expiring << [key, expdays]
         end
 
-        if key.trust
-          unusable << [key, key.trust]
+        if ! key.usable?
+          unusable << [key, key.usability_issue]
         end
       end
 
@@ -154,9 +154,9 @@ module Schleuder
                       })
       end
 
-      unusable.each do |key,trust|
+      unusable.each do |key,usability_issue|
         text << I18n.t('key_unusable', {
-                          trust: Array(trust).join(', '),
+                          usability_issue: usability_issue,
                           fingerprint: key.fingerprint,
                           email: key.email
                       })
@@ -164,6 +164,10 @@ module Schleuder
       text
     end
 
+    def refresh_keys
+      GPGME::Ctx.refresh_keys(self.keys)
+    end
+
     def self.by_recipient(recipient)
       listname = recipient.gsub(/-(sendkey|request|owner|bounce)@/, '@')
       where(email: listname).first
@@ -228,14 +232,16 @@ module Schleuder
     end
 
     def subscribe(email, fingerprint=nil, adminflag=false, deliveryflag=true)
-      adminflag ||= false
-      deliveryflag ||= true
+      # Ensure we have true or false as values for these two attributes.
+      admin            = adminflag.to_s == 'true'
+      delivery_enabled = deliveryflag.to_s != 'false'
+
       sub = Subscription.new(
           list_id: self.id,
           email: email,
           fingerprint: fingerprint,
-          admin: adminflag,
-          delivery_enabled: deliveryflag
+          admin: admin,
+          delivery_enabled: delivery_enabled
         )
       sub.save
       sub
@@ -271,7 +277,7 @@ module Schleuder
     def from_admin?(mail)
       return false if ! mail.was_validly_signed?
       admins.find do |admin|
-        admin.fingerprint == mail.signature.fingerprint
+        admin.fingerprint == mail.signing_key.fingerprint
       end.presence || false
     end
 
@@ -285,13 +291,17 @@ module Schleuder
       ENV['GNUPGHOME'] = listdir
     end
 
-    def delete_listdir
+    def delete_listdirs
       if File.exists?(self.listdir)
         FileUtils.rm_rf(self.listdir, secure: true)
-        Schleuder.logger.info "Deleted listdir"
-      else
-        # Don't use list-logger here — if the list-dir isn't present we can't log to it!
-        Schleuder.logger.info "Couldn't delete listdir, directory not present"
+        Schleuder.logger.info "Deleted #{self.listdir}"
+      end
+      # If listlogs_dir is different from lists_dir, the logfile still exists
+      # and needs to be deleted, too.
+      logfile_dir = File.dirname(self.logfile)
+      if File.exists?(logfile_dir)
+        FileUtils.rm_rf(logfile_dir, secure: true)
+        Schleuder.logger.info "Deleted #{logfile_dir}"
       end
       true
     rescue => exc
diff --git a/lib/schleuder/list_builder.rb b/lib/schleuder/list_builder.rb
index f9599ce..5387bda 100644
--- a/lib/schleuder/list_builder.rb
+++ b/lib/schleuder/list_builder.rb
@@ -107,6 +107,12 @@ module Schleuder
           raise err
         end
       end
+      # Go through list.key() to re-fetch the key from the keyring, otherwise
+      # we don't see the new UIDs.
+      errors = list.key.set_primary_uid(list.email)
+      if errors.present?
+        raise errors
+      end
     rescue => exc
       raise Errors::KeyAdduidFailed.new(exc.to_s)
     end
@@ -116,8 +122,10 @@ module Schleuder
         <GnupgKeyParms format=\"internal\">
         Key-Type: RSA
         Key-Length: 4096
+        Key-Usage: sign
         Subkey-Type: RSA
         Subkey-Length: 4096
+        Subkey-Usage: encrypt
         Name-Real: #{list.email}
         Name-Email: #{list.email}
         Expire-Date: 0
diff --git a/lib/schleuder/listlogger.rb b/lib/schleuder/listlogger.rb
index 67939cd..36cdcdd 100644
--- a/lib/schleuder/listlogger.rb
+++ b/lib/schleuder/listlogger.rb
@@ -4,7 +4,8 @@ module Schleuder
     def initialize(list)
       super(list.logfile, 'daily')
       @from = list.email
-      @adminaddresses = list.admins.map(&:email)
+      @list = list
+      @adminaddresses = list.admins.map { |sub| [sub.email, sub.key] }
       @level = ::Logger.const_get(list.log_level.upcase)
       remove_old_logfiles(list)
     end
diff --git a/lib/schleuder/logger_notifications.rb b/lib/schleuder/logger_notifications.rb
index c503017..c5a4d18 100644
--- a/lib/schleuder/logger_notifications.rb
+++ b/lib/schleuder/logger_notifications.rb
@@ -15,7 +15,8 @@ module Schleuder
     end
 
     def notify_admin(string, original_message=nil, subject='Error')
-      Array(adminaddresses).each do |address|
+      # Minimize using other classes here, we don't know what caused the error.
+      Array(adminaddresses).each do |address, key|
         mail = Mail.new
         mail.from = @from
         mail.to = address
@@ -31,6 +32,13 @@ module Schleuder
           orig_part.body = original_message.to_s
           mail.add_part orig_part
         end
+        if @list.present?
+          gpg_opts = @list.gpg_sign_options
+          if key.present? && key.usable?
+            gpg_opts.merge!(encrypt: true, keys: { address => key.fingerprint })
+          end
+          mail.gpg gpg_opts
+        end
         mail.deliver
       end
       true
diff --git a/lib/schleuder/mail/message.rb b/lib/schleuder/mail/message.rb
index 8b9aa86..eafe53b 100644
--- a/lib/schleuder/mail/message.rb
+++ b/lib/schleuder/mail/message.rb
@@ -106,8 +106,17 @@ module Mail
     end
 
     def signer
-      if fingerprint = self.signature.try(:fpr)
-        list.subscriptions.where(fingerprint: fingerprint).first
+      if signing_key.present?
+        list.subscriptions.where(fingerprint: signing_key.fingerprint).first
+      end
+    end
+
+    # The fingerprint of the signature might be the one of a sub-key, but the
+    # subscription-assigned fingerprints are (should be) the ones of the
+    # primary keys, so we need to look up the key.
+    def signing_key
+      if signature.present?
+        @signing_key ||= list.keys(signature.fpr).first
       end
     end
 
@@ -208,7 +217,7 @@ module Mail
         # Some versions of gpgme return nil if the key is unknown, so we check
         # for that manually and provide our own fallback. (Calling
         # `signature.key` results in an EOFError in that case.)
-        if list.key(signature.fingerprint)
+        if signing_key.present?
           msg = signature.to_s
         else
           # TODO: I18n
diff --git a/lib/schleuder/plugins/key_management.rb b/lib/schleuder/plugins/key_management.rb
index 94f8ad8..e3184a8 100644
--- a/lib/schleuder/plugins/key_management.rb
+++ b/lib/schleuder/plugins/key_management.rb
@@ -1,17 +1,17 @@
 module Schleuder
   module RequestPlugins
     def self.add_key(arguments, list, mail)
-      key_material = if mail.parts.first.present?
-                       mail.parts.first.body
-                     else
-                       mail.body
-                     end.to_s
-      result = list.import_key(key_material)
-
       out = [I18n.t('plugins.key_management.import_result')]
-      out << result.map do |import_result|
-        str = I18n.t("plugins.key_management.key_import_status.#{import_result.action}")
-        "#{import_result.fpr}: #{str}"
+
+      if mail.has_attachments?
+        results = self.import_keys_from_attachments(list, mail)
+      else
+        results = [self.import_key_from_body(list, mail)]
+      end
+
+      out << results.compact.collect(&:imports).flatten.map do |import_status|
+        str = I18n.t("plugins.key_management.key_import_status.#{import_status.action}")
+        "#{import_status.fpr}: #{str}"
       end
     end
 
@@ -47,5 +47,40 @@ module Schleuder
         hkp.fetch_and_import(argument)
       end
     end
+
+    def self.is_armored_key?(material)
+      return false unless /^-----BEGIN PGP PUBLIC KEY BLOCK-----$/ =~ material
+      return false unless /^-----END PGP PUBLIC KEY BLOCK-----$/ =~ material
+
+      lines = material.split("\n").reject(&:empty?)
+      # remove header
+      lines.shift
+      # remove tail
+      lines.pop
+      # verify the rest
+      # TODO: verify length except for lasts lines?
+      # headers according to https://tools.ietf.org/html/rfc4880#section-6.2
+      lines.map do |line|
+        /\A((comment|version|messageid|hash|charset):.*|[0-9a-z\/=+]+)\Z/i =~ line
+      end.all?
+    end
+
+    def self.import_keys_from_attachments(list, mail)
+      mail.attachments.map do |attachment|
+        material = attachment.body.to_s
+
+        list.import_key(material) if self.is_armored_key?(material)
+      end
+    end
+
+    def self.import_key_from_body(list, mail)
+      if mail.parts.first.present?
+        key_material = mail.parts.first.body.to_s
+      else
+        key_material = mail.body.to_s
+      end
+
+      list.import_key(key_material) if self.is_armored_key?(key_material)
+    end
   end
 end
diff --git a/lib/schleuder/plugins/subscription_management.rb b/lib/schleuder/plugins/subscription_management.rb
index 5f9359e..c1e3aee 100644
--- a/lib/schleuder/plugins/subscription_management.rb
+++ b/lib/schleuder/plugins/subscription_management.rb
@@ -1,22 +1,29 @@
 module Schleuder
   module RequestPlugins
     def self.subscribe(arguments, list, mail)
-      sub = list.subscriptions.new(
-        email: arguments.first,
-        fingerprint: arguments.last
-      )
+      email = arguments.shift
+      fingerprint = arguments.shift
+      if fingerprint.present?
+        fingerprint.sub!(/^0x/, '')
+      end
+      adminflag = arguments.shift
+      deliveryflag = arguments.shift
+
+      sub = list.subscribe(email, fingerprint, adminflag, deliveryflag)
 
-      if sub
+      if sub.persisted?
         I18n.t(
           "plugins.subscription_management.subscribed",
           email: sub.email,
-          fingerprint: sub.fingerprint
+          fingerprint: sub.fingerprint,
+          admin: sub.admin,
+          delivery_enabled: sub.delivery_enabled
         )
       else
         I18n.t(
           "plugins.subscription_management.subscribing_failed",
           email: sub.email,
-          errors: sub.errors.full_messages
+          errors: sub.errors.full_messages.join(".\n")
         )
       end
     end
@@ -70,7 +77,10 @@ module Schleuder
       out << subs.map do |subscription|
         # Fingerprints are at most 40 characters long, and lines shouldn't
         # exceed 80 characters if possible.
-        s = "#{subscription.email}\t0x#{subscription.fingerprint}"
+        s = subscription.email
+        if subscription.fingerprint.present?
+          s << "\t0x#{subscription.fingerprint}"
+        end
         if ! subscription.delivery_enabled?
           s << "\tDelivery disabled!"
         end
diff --git a/lib/schleuder/plugins_runner.rb b/lib/schleuder/plugins_runner.rb
index f7ef3bd..399a16e 100644
--- a/lib/schleuder/plugins_runner.rb
+++ b/lib/schleuder/plugins_runner.rb
@@ -12,11 +12,14 @@ module Schleuder
         else
           @plugin_module = ListPlugins
         end
+
+        check_listname_keyword
+
         mail.keywords.each do |keyword, arguments|
           @list.logger.debug "Running keyword '#{keyword}'"
           if @list.admin_only?(keyword) && ! @list.from_admin?(@mail)
             @list.logger.debug "Error: Keyword is admin-only, sent by non-admin"
-            output << Schleuder::Errors::KeywordAdminOnly.new(keyword)
+            output << Schleuder::Errors::KeywordAdminOnly.new(keyword).to_s
             next
           end
           output << run_plugin(keyword, arguments)
@@ -26,7 +29,11 @@ module Schleuder
 
       def self.run_plugin(keyword, arguments)
         command = keyword.gsub('-', '_')
-        if @plugin_module.respond_to?(command)
+        if command == 'listname'
+          return nil
+        elsif ! @plugin_module.respond_to?(command)
+          return I18n.t('plugins.unknown_keyword', keyword: keyword)
+        else
           out = @plugin_module.send(command, arguments, @list, @mail)
           response = Array(out).flatten.join("\n\n")
           if @list.keywords_admin_notify.include?(keyword)
@@ -37,9 +44,7 @@ module Schleuder
                                 )
             @list.logger.notify_admin("#{explanation}\n\n#{response}\n", nil, 'Notice')
           end
-          response
-        else
-          I18n.t('plugins.unknown_keyword', keyword: keyword)
+          return response
         end
       rescue => exc
         # Log to system, this information is probably more useful for
@@ -54,6 +59,23 @@ module Schleuder
           require file
         end
       end
+
+      def self.check_listname_keyword
+        return nil if @mail.keywords.blank?
+
+        listname_kw = @mail.keywords.assoc('listname')
+        if listname_kw.blank?
+          @mail.reply_to_signer I18n.t(:missing_listname_keyword_error)
+          exit
+        else
+          listname_args = listname_kw.last
+          if ! [@list.email, @list.request_address].include?(listname_args.first)
+            @mail.reply_to_signer I18n.t(:wrong_listname_keyword_error)
+            exit
+          end
+        end
+
+      end
     end
 
   end
diff --git a/lib/schleuder/runner.rb b/lib/schleuder/runner.rb
index 089e86a..e9638ae 100644
--- a/lib/schleuder/runner.rb
+++ b/lib/schleuder/runner.rb
@@ -68,7 +68,9 @@ module Schleuder
         begin
           subscription.send_mail(new)
         rescue => exc
-          logger.error exc
+          msg = I18n.t('errors.delivery_error',
+                       { email: subscription.email, error: exc.to_s })
+          logger.error msg
         end
       end
     end
diff --git a/lib/schleuder/subscription.rb b/lib/schleuder/subscription.rb
index 306f8b0..fb9e4e4 100644
--- a/lib/schleuder/subscription.rb
+++ b/lib/schleuder/subscription.rb
@@ -41,16 +41,26 @@ module Schleuder
       end
 
       mail = ensure_headers(mail)
-      gpg_opts = self.list.gpg_sign_options.merge(encrypt: true, keys: {self.email => "0x#{self.fingerprint}"})
+      gpg_opts = self.list.gpg_sign_options
+
       if self.key.blank?
         if self.list.send_encrypted_only?
-          self.list.logger.warn "Not sending to #{self.email}: no key present and sending plain text not allowed"
-          notify_of_missed_message
+          notify_of_missed_message(:absent)
+          return false
+        else
+          list.logger.warn "Sending plaintext because no key is present!"
+        end
+      elsif ! self.key.usable?
+        if self.list.send_encrypted_only?
+          notify_of_missed_message(key.usability_issue)
           return false
         else
-          gpg_opts.merge!(encrypt: false)
+          list.logger.warn "Sending plaintext because assigned key is #{key.usability_issue}!"
         end
+      else
+        gpg_opts.merge!(encrypt: true, keys: {self.email => "0x#{self.fingerprint}"})
       end
+
       list.logger.info "Sending message to #{self.email}"
       mail.gpg gpg_opts
       mail.deliver
@@ -63,10 +73,11 @@ module Schleuder
       mail
     end
 
-    def notify_of_missed_message
+    def notify_of_missed_message(reason)
+      self.list.logger.warn "Not sending to #{self.email}: key is unusable because it is #{reason} and sending plain text not allowed"
       mail = ensure_headers(Mail.new)
       mail.subject = I18n.t('notice')
-      mail.body = I18n.t("missed_message_due_to_absent_key", list_email: self.list.email) + I18n.t('errors.signoff')
+      mail.body = I18n.t("missed_message_due_to_unusable_key", list_email: self.list.email) + I18n.t('errors.signoff')
       mail.gpg self.list.gpg_sign_options
       mail.deliver
     end
diff --git a/lib/schleuder/version.rb b/lib/schleuder/version.rb
index 8286dee..7efefc1 100644
--- a/lib/schleuder/version.rb
+++ b/lib/schleuder/version.rb
@@ -1,3 +1,3 @@
 module Schleuder
-  VERSION = '3.0.0.beta17'
+  VERSION = '3.0.0'
 end
diff --git a/locales/de.yml b/locales/de.yml
index 6788e43..6b419cf 100644
--- a/locales/de.yml
+++ b/locales/de.yml
@@ -10,8 +10,8 @@ de:
       public_footer:
         invalid: "enthält nicht druckbare Zeichen"
     invalid_email: "ist keine valide E-Mail-Adresse"
-    invalid_fingerprint: "ist kein valider Fingerprint"
-    fingerprint_blank: 'Fingerprint der Liste ist nicht gesetzt, kann nicht arbeiten!'
+    invalid_fingerprint: "ist kein valider OpenPGP-Fingerabdruck"
+    fingerprint_blank: 'Fingerabdruck der Liste ist nicht gesetzt, kann nicht arbeiten!'
     list_key_missing: 'Schlüssel der Liste nicht im Schlüsselring gefunden, kann nicht arbeiten!'
     list_secret_key_missing: 'Geheimer Schlüssel der Liste nicht im Schlüsselring gefunden, kann nicht arbeiten!'
     admins_missing: 'List hat keine Admins konfiguriert, kann nicht arbeiten!'
@@ -70,6 +70,7 @@ de:
     must_be_greater_than_zero: "muss größer als null sein"
     file_not_found: "Die Datei existiert nicht: '%{file}'."
     not_pgp_mime: "Deine Email war nicht im pgp/mime-Format verschlüsselt."
+    delivery_error: "Beim Versenden einer Email an %{email} ist der folgende Fehler aufgetreten: %{error}"
   plugins:
     unknown_keyword: Unbekanntes Schlüsselwort '%{keyword}'.
     plugin_failed: Das Schlüsselwort '%{keyword}' verursachte einen unbekannten Fehler. Die System-Administratoren wurden benachrichtigt.
@@ -91,10 +92,16 @@ de:
       unsubscribing_failed: |
         Abo für %{email} nicht gelöscht:
         %{errors}
-      subscribed: Abo für %{email} mit Fingerabdruck %{fingerprint} eingetragen.
+      subscribed: |
+        Abo für %{email} mit diesen Werten eingetragen:
+
+        Fingerabdruck: %{fingerprint}
+        Admin? %{admin}
+        Email-Zustellung aktiv? %{delivery_enabled}
       subscribing_failed: |
         Abo für %{email} nicht eingetragen:
-        %{errors}
+
+        %{errors}.
       list_of_subscriptions: Abos
       fingerprint_set: Fingerabdruck für %{email} auf %{fingerprint} gesetzt.
       setting_fingerprint_failed: |
@@ -105,6 +112,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.
   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:"
@@ -116,5 +125,14 @@ de:
   check_keys: Schlüsselprüfung
   check_keys_intro: "Bitte kümmere dich um die folgenden Schlüssel für Liste %{email}."
   key_expires: "Läuft in %{days} Tagen ab:\n0x%{fingerprint} %{email}"
-  key_unusable: "Ist %{trust}:\n0x%{fingerprint} %{email}"
-  missed_message_due_to_absent_key: "Du hast eine Email von %{list_email} verpasst weil mit deinem Abo keinen (benutzbarer) OpenPGP-Schlüssel verknüpft ist. Bitte kümmere dich darum."
+  key_unusable: "Ist %{usability_issue}:\n0x%{fingerprint} %{email}"
+  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:"
+  key_updated: Schlüssel %{fingerprint} wurde aktualisiert (%{states}).
+  import_states:
+    new_keys: neue Schlüssel
+    new_uids: neue User-IDs
+    new_subkeys: neue Unterschlüssel
+    new_signatures: neue Signaturen
+
diff --git a/locales/en.yml b/locales/en.yml
index 53b2d8c..b1c5758 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -10,7 +10,7 @@ en:
       public_footer:
         invalid: "includes non-printable characters"
     invalid_email: "is not a valid email address"
-    invalid_fingerprint: "is not a valid fingerprint"
+    invalid_fingerprint: "is not a valid OpenPGP-fingerprint"
     list_fingerprint_missing: "List has no fingerprint configured, cannot run!"
     list_key_missing: "List-key is missing in keyring, cannot run!"
     list_secret_key_missing: 'Secret key of list is missing in keyring, cannot run!'
@@ -70,6 +70,7 @@ en:
     must_be_greater_than_zero: "must be a number greater than zero"
     file_not_found: "File not found: '%{file}'."
     not_pgp_mime: "Message was not encrypted in the pgp/mime-format."
+    delivery_error: "The following error occurred while sending a message to %{email}: %{error}"
   plugins:
     unknown_keyword: Unknown keyword '%{keyword}'.
     plugin_failed: Running keyword '%{keyword}' caused an unknown error. System-admins have been notified.
@@ -91,10 +92,16 @@ en:
       unsubscribing_failed: |
         Unsubscribing %{email} failed:
         %{errors}
-      subscribed: "%{email} has been subscribed with fingerprint %{fingerprint}."
+      subscribed: |
+        %{email} has been subscribed with these attributes:
+
+        Fingerprint: %{fingerprint}
+        Admin? %{admin}
+        Email-delivery enabled? %{delivery_enabled}
       subscribing_failed: |
         Subscribing %{email} failed:
-        %{errors}
+
+        %{errors}.
       list_of_subscriptions: Subscriptions
       fingerprint_set: Fingerprint for %{email} set to %{fingerprint}.
       setting_fingerprint_failed: |
@@ -105,6 +112,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 didn't contain a wrong X-LISTNAME-keyword. The value of that keyword muss 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:"
@@ -116,5 +125,14 @@ en:
   check_keys: Keys check
   check_keys_intro: "Please take care of these keys for list %{email}.\n"
   key_expires: "Expires in %{days} days:\n0x%{fingerprint} %{email}"
-  key_unusable: "Is %{trust}:\n0x%{fingerprint} %{email}"
-  missed_message_due_to_absent_key: "You missed an email from %{list_email} because your subscription isn't associated with a (usable) OpenPGP key. Please fix this."
+  key_unusable: "Is %{usability_issue}:\n0x%{fingerprint} %{email}"
+  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:"
+  key_updated: Key %{fingerprint} was updated (%{states}).
+  import_states:
+    new_keys: new keys
+    new_uids: new user-IDs
+    new_subkeys: new subkeys
+    new_signatures: new signatures
+
diff --git a/man/schleuder-api-daemon.8 b/man/schleuder-api-daemon.8
index 74f12ec..91cd4c5 100644
--- a/man/schleuder-api-daemon.8
+++ b/man/schleuder-api-daemon.8
@@ -1,16 +1,13 @@
 .\" generated with Ronn/v0.7.3
-.\" https://github.com/rtomayko/ronn/tree/0.7.3
+.\" http://github.com/rtomayko/ronn/tree/0.7.3
 .
-.TH "SCHLEUDER\-API\-DAEMON" "8" "November 2016" "" ""
-.
-.SH "NAME"
-\fBschleuder\-api\-daemon\fR \- HTTP\-API of Schleuder(8)
+.TH "SCHLEUDER\-API\-DAEMON" "8" "January 2017" "" ""
 .
 .SH "SYNOPSIS"
 \fBschleuder\-api\-daemon\fR
 .
 .SH "DESCRIPTION"
-schleuder\-api\-daemon provides the HTTP\-API of Schleuder(8) to clients\.
+schleuder\-api\-daemon provides the HTTP\-API of \fBschleuder(8)\fR to clients\.
 .
 .SH "ENVIRONMENT"
 .
@@ -22,33 +19,29 @@ The available options are:
 .
 .TP
 \fBhost\fR
-The hostname/IP to listen at\. This is overwritten with \'localhost\' unless \fBuse_tls\fR is true\.
+The hostname/IP to listen at\.
 .
 .TP
 \fBport\fR
 The port to listen at\. Default: 4443\.
 .
 .TP
-\fBuse_tls\fR
-Serve the API via HTTPS? Default: false\. Requires a usable certificate and key specified as \fBtls_cert_file\fR and \fBtls_key_file\fR\.
-.
-.TP
 \fBtls_cert_file\fR
-Path to the file that contains the TLS\-certificate to use for HTTPS\. You can generate a new one with \fBschleuder cert generate\fR\.
+Path to the file that contains the TLS\-certificate to use\. You can generate a new one with \fBschleuder cert generate\fR\.
 .
 .TP
 \fBtls_key_file\fR
-Path to the file that contains the TLS\-key to use for HTTPS\.
+Path to the file that contains the TLS\-key to use\.
 .
 .TP
 \fBvalid_api_keys\fR
 List of api_keys to allow access to the API\.
 .
 .SS "Clients"
-Available clients using the API are \fBschleuder\-cli\fR(8) and \fBschleuder\-web\fR\. URLs to their websites are listed in \fISEE ALSO\fR\.
+Available clients using the API are \fBschleuder\-cli\fR(8) and \fBschleuder\-web\fR\. URLs to their websites are listed below (\fISEE ALSO\fR)\.
 .
 .SH "BUGS"
-Known bugs are listed on the Schleuder bugtracker at \fIhttps://codecoop\.org/schleuder/schleuder3\fR
+Known bugs are listed on the Schleuder bugtracker at \fIhttps://0xacab\.org/schleuder/schleuder\fR
 .
 .SH "SEE ALSO"
 \fBschleuder\fR(8), \fBschleuder\-cli\fR(8)
@@ -63,9 +56,9 @@ More extensive documentation for \fBschleuder\fR
 .
 .TP
 \fBschleuder\-cli\fR, the command line interface for list\-management
-\fIhttps://codecoop\.org/schleuder/schleuder\-cli/\fR
+\fIhttps://0xacab\.org/schleuder/schleuder\-cli/\fR
 .
 .TP
 \fBschleuder\-web\fR, the web interface for list\-management
-\fIhttps://codecoop\.org/schleuder/schleuder\-web/\fR
+\fIhttps://0xacab\.org/schleuder/schleuder\-web/\fR
 
diff --git a/man/schleuder-api-daemon.8.ron b/man/schleuder-api-daemon.8.ron
index 0e3a088..f674c8f 100644
--- a/man/schleuder-api-daemon.8.ron
+++ b/man/schleuder-api-daemon.8.ron
@@ -19,15 +19,13 @@ schleuder-api-daemon provides the HTTP-API of `schleuder(8)` to clients.
 The available options are:
 
  * `host`:
-   The hostname/IP to listen at. This is overwritten with 'localhost' unless `use_tls` is true.
+   The hostname/IP to listen at.
  * `port`:
    The port to listen at. Default: 4443.
- * `use_tls`:
-   Serve the API via HTTPS? Default: false. Requires a usable certificate and key specified as `tls_cert_file` and `tls_key_file`.
  * `tls_cert_file`:
-   Path to the file that contains the TLS-certificate to use for HTTPS. You can generate a new one with `schleuder cert generate`.
+   Path to the file that contains the TLS-certificate to use. You can generate a new one with `schleuder cert generate`.
  * `tls_key_file`:
-   Path to the file that contains the TLS-key to use for HTTPS.
+   Path to the file that contains the TLS-key to use.
  * `valid_api_keys`:
    List of api_keys to allow access to the API.
 
@@ -40,7 +38,7 @@ Available clients using the API are `schleuder-cli`(8) and `schleuder-web`. URLs
 ## BUGS
 
 Known bugs are listed on the Schleuder bugtracker at
-<https://codecoop.org/schleuder/schleuder3>
+<https://0xacab.org/schleuder/schleuder>
 
 ## SEE ALSO
 
@@ -54,8 +52,8 @@ Known bugs are listed on the Schleuder bugtracker at
    <https://schleuder.nadir.org/docs/>
 
  * `schleuder-cli`, the command line interface for list-management:
-   <https://codecoop.org/schleuder/schleuder-cli/>
+   <https://0xacab.org/schleuder/schleuder-cli/>
 
  * `schleuder-web`, the web interface for list-management:
-   <https://codecoop.org/schleuder/schleuder-web/>
+   <https://0xacab.org/schleuder/schleuder-web/>
 
diff --git a/man/schleuder.8 b/man/schleuder.8
index ed99bbf..a4bd1c1 100644
--- a/man/schleuder.8
+++ b/man/schleuder.8
@@ -1,7 +1,7 @@
 .\" generated with Ronn/v0.7.3
 .\" http://github.com/rtomayko/ronn/tree/0.7.3
 .
-.TH "SCHLEUDER" "8" "December 2016" "" ""
+.TH "SCHLEUDER" "8" "January 2017" "" ""
 .
 .SH "NAME"
 \fBschleuder\fR \- an email hub for groups
@@ -64,7 +64,7 @@ To connect the MTA with Schleuder it must pipe the incoming message into Schleud
 For more information on how to integrate Schleuder with your existing mail setup, please read the Schleuder documentation online (\fISEE ALSO\fR)\.
 .
 .SS "Data storage"
-The keyrings for each list are standard GnuPG keyrings and sit in the filesystem under \fIlists_dir\fR/\fIhostname\fR/\fIlistname\fR/ (\fIlists_dir\fR is read from schleuder\.yml, by default it is </var/schleuder/lists>)\. They can be used manually using gpg2\. Please be careful to maintain proper file permissions if you touch the files\.
+The keyrings for each list are standard GnuPG keyrings and sit in the filesystem under \fIlists_dir\fR/\fIhostname\fR/\fIlistname\fR/ (\fIlists_dir\fR is read from schleuder\.yml, by default it is </var/lib/schleuder/lists>)\. They can be used manually using gpg2\. Please be careful to maintain proper file permissions if you touch the files\.
 .
 .P
 In the list\-directory there’s also a list specific log\-file (might be missing if the log\-level is high and no error occurred yet)\.
@@ -135,7 +135,7 @@ Internal failure in incoming email processing\.
 \fB/etc/schleuder/list\-defaults\.yml\fR: default path of default list settings
 .
 .IP "\(bu" 4
-\fB/var/schleuder/\fR default path of lists_dir
+\fB/var/lib/schleuder/lists\fR default path of lists_dir
 .
 .IP "\(bu" 4
 \fB<lists_dir>\fR/\fB<hostname>\fR/`\fIlistname\fR: list internal data
@@ -149,7 +149,7 @@ Internal failure in incoming email processing\.
 All configuration files are formatted as YAML\. See \fIhttp://www\.yaml\.org/\fR for more details\.
 .
 .SH "BUGS"
-Known bugs are listed on the Schleuder bugtracker at \fIhttps://codecoop\.org/schleuder/schleuder\fR
+Known bugs are listed on the Schleuder bugtracker at \fIhttps://0xacab\.org/schleuder/schleuder\fR
 .
 .SH "SEE ALSO"
 \fBschleuder\-cli\fR(8), \fBgnupg\fR(7)\.
@@ -164,9 +164,9 @@ More extensive documentation for \fBschleuder\fR
 .
 .TP
 \fBschleuder\-cli\fR, the command line interface for list\-management
-\fIhttps://codecoop\.org/schleuder/schleuder\-cli/\fR
+\fIhttps://0xacab\.org/schleuder/schleuder\-cli/\fR
 .
 .TP
 \fBschleuder\-web\fR, the web interface for list\-management
-\fIhttps://codecoop\.org/schleuder/schleuder\-web/\fR
+\fIhttps://0xacab\.org/schleuder/schleuder\-web/\fR
 
diff --git a/man/schleuder.8.ron b/man/schleuder.8.ron
index 328ce7d..82b6fb8 100644
--- a/man/schleuder.8.ron
+++ b/man/schleuder.8.ron
@@ -47,7 +47,7 @@ setup, please read the Schleuder documentation online ([SEE ALSO][]).
 
 ### Data storage
 
-The keyrings for each list are standard GnuPG keyrings and sit in the filesystem under <lists_dir>/<hostname>/<listname>/ (<lists_dir> is read from schleuder.yml, by default it is </var/schleuder/lists>). They can be used manually using gpg2. Please be careful to maintain proper file permissions if you touch the files.
+The keyrings for each list are standard GnuPG keyrings and sit in the filesystem under <lists_dir>/<hostname>/<listname>/ (<lists_dir> is read from schleuder.yml, by default it is </var/lib/schleuder/lists>). They can be used manually using gpg2. Please be careful to maintain proper file permissions if you touch the files.
 
 In the list-directory there’s also a list specific log-file (might be missing if the log-level is high and no error occurred yet).
 
@@ -104,7 +104,7 @@ Write to <listname-owner at hostname> to contact the list-owner(s) even if you don'
  * `/etc/schleuder/list-defaults.yml`:
    default path of default list settings
 
- * `/var/schleuder/`
+ * `/var/lib/schleuder/lists`
    default path of lists_dir
 
  * `<lists_dir>`/`<hostname>`/`<listname>:
@@ -119,7 +119,7 @@ All configuration files are formatted as YAML. See
 ## BUGS
 
 Known bugs are listed on the Schleuder bugtracker at
-<https://codecoop.org/schleuder/schleuder>
+<https://0xacab.org/schleuder/schleuder>
 
 ## SEE ALSO
 
@@ -133,7 +133,7 @@ Known bugs are listed on the Schleuder bugtracker at
    <https://schleuder.nadir.org/docs/>
 
  * `schleuder-cli`, the command line interface for list-management:
-   <https://codecoop.org/schleuder/schleuder-cli/>
+   <https://0xacab.org/schleuder/schleuder-cli/>
 
  * `schleuder-web`, the web interface for list-management:
-   <https://codecoop.org/schleuder/schleuder-web/>
+   <https://0xacab.org/schleuder/schleuder-web/>
diff --git a/spec/fixtures/expired_key.txt b/spec/fixtures/expired_key.txt
new file mode 100644
index 0000000..4e64973
--- /dev/null
+++ b/spec/fixtures/expired_key.txt
@@ -0,0 +1,29 @@
+-----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
+P8FCKvImh840vTbpgFSgQeaJzJFv9UrloNyyvbVtaeoxnxoBuQENBExlilkBCADD
+Gbf0TEs5HDpbUC78tJetGWipmQRTq8gS9dDoKjG5mvlpFARPTAJvewgI7DICXMtV
+Y8P8eFbsZDGMETduunadutDvyP9J/wuknYHJkE5jJeZjEKyofrWM1BHxHb2bkNU0
+hr8EVNvEhVjAb62cJPj5emi/3UhynLoJrrO+AZSr9Bq2QW38ntSUxTXM7VbGUR1M
+WZSoJ+gEg/IeR2HJziCyb1mhlvo9pZqJT959bNVQ/xU4NHtYca5cV3X88Ald4Dwz
+2HM+PIBbsiz3C3fIMNrRxRvMCU4PgsjSZFoRxcdDRT5OjBxNgQ4UmWQH0sTFw9Tx
+8B2OtwuguGwaXIV3FpRDABEBAAGJASUEGAECAA8FAkxlilkCGwwFCQABUYAACgkQ
+9xo/hBLYOImqngf/QQt0S6tlJ9OMknmAr2pNg+DQkCqTNaSk/iQj4leGGwkpRVoH
+5VFTZ0nkmZjcDTTrCjj5rEDaRo8Q38KsB1po8P25ABoK0b28rHw8I3L2Byl1+IB7
++dNKVyFJVfHAYOQsbI/p/2KdtZZIbpxnRVHY+Vlu2p/fx3JqPmqCiaVMcUFw55Qj
+SKaI+omfnN0WGyrK1Rub925Lch0vZkxVwmTse7qufg0iwTREMy9VZfMavMhkAtM2
+AsiEG8j3mwU9JQfBkXqtWxG2VvtsBJ0rLafh5sR3NgjtR2vbSdzJRCV2xO4z6Drh
+3Pug3ReHmcLUFTDPE+vmeH+xpEZ2nhvNRFDhmQ==
+=Nao0
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/spec/fixtures/expired_key_extended.txt b/spec/fixtures/expired_key_extended.txt
new file mode 100644
index 0000000..1587675
--- /dev/null
+++ b/spec/fixtures/expired_key_extended.txt
@@ -0,0 +1,29 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: SKS 1.1.6+
+Comment: Hostname: sks.spodhuis.org
+
+mQENBExlilkBCACb2AQyclf7latAIE1kCTfKQ9jmcKyf959ymyhzoeNmBDpKjILC7MOXtICo
+/V/xAzhWBK/vT9+56brGUBTugnW3yK+zllQprI3kIYaRS1SrbmKVwVse9qLVUL1BssohFaEe
+QqT4MNh62ziJymqCguGEGXpYlEqzEDTmmhTANiPKRBZDrdfq3FU3OJUMTGzuG34mKmXMRr0a
+zprF228LUujMMKyWhG1hxh3El04C4jPuMSbaVcwNE2rgIg8jaNAQuSyXkaprPZ8/nRG8UFGR
+tCMEIEh6Ou6KybF1NI9LQKCwcsGcLHKU2u/8vOCExxdwl9Jjlqmof4FQV7bT++6SC0n3ABEB
+AAG0EWV4cGlyZWQgPGJsYUBmb28+iQFVBBMBAgA/AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIe
+AQIXgBYhBJh2nooQkfNr2IQD7PcaP4QS2DiJBQJYgOG1BQkMHGNXAAoJEPcaP4QS2DiJxiUH
+/2xYCT+mldoaWMyJlBleSjx0wtWeGNayMuv0RjU2pIkBmslYp/ZZkt+JC3thpJneBW5pJuRB
+qeUi7yTigdtOrH47g+FfIDCY89ymTDbYW4vpNnnsV7s+ke8tbEmTtMpjFypoTvbnGYlq8VLz
+87eRcsLwADOAJfFBdnDD0tyNCrUY/V+Ti/ZI4bHoFA14t8Hm7MIDkfB6sVfzpnZd1ACj+klv
+1rfq+9m56lsavS/dM+BlhwfRORT9cenuBs++AXXWvh1CZW/J06kFECG+ptqU5246nQcjE5GX
+W8sC+TSq7OXSTQAJDF+aWqjA/JrbpSf/3r2/IU+mGH2Bwi7B5uBN6lG5AQ0ETGWKWQEIAMMZ
+t/RMSzkcOltQLvy0l60ZaKmZBFOryBL10OgqMbma+WkUBE9MAm97CAjsMgJcy1Vjw/x4Vuxk
+MYwRN266dp260O/I/0n/C6SdgcmQTmMl5mMQrKh+tYzUEfEdvZuQ1TSGvwRU28SFWMBvrZwk
++Pl6aL/dSHKcugmus74BlKv0GrZBbfye1JTFNcztVsZRHUxZlKgn6ASD8h5HYcnOILJvWaGW
++j2lmolP3n1s1VD/FTg0e1hxrlxXdfzwCV3gPDPYcz48gFuyLPcLd8gw2tHFG8wJTg+CyNJk
+WhHFx0NFPk6MHE2BDhSZZAfSxMXD1PHwHY63C6C4bBpchXcWlEMAEQEAAYkBJQQYAQIADwUC
+TGWKWQIbDAUJAAFRgAAKCRD3Gj+EEtg4iaqeB/9BC3RLq2Un04ySeYCvak2D4NCQKpM1pKT+
+JCPiV4YbCSlFWgflUVNnSeSZmNwNNOsKOPmsQNpGjxDfwqwHWmjw/bkAGgrRvbysfDwjcvYH
+KXX4gHv500pXIUlV8cBg5Cxsj+n/Yp21lkhunGdFUdj5WW7an9/Hcmo+aoKJpUxxQXDnlCNI
+poj6iZ+c3RYbKsrVG5v3bktyHS9mTFXCZOx7uq5+DSLBNEQzL1Vl8xq8yGQC0zYCyIQbyPeb
+BT0lB8GReq1bEbZW+2wEnSstp+HmxHc2CO1Ha9tJ3MlEJXbE7jPoOuHc+6DdF4eZwtQVMM8T
+6+Z4f7GkRnaeG81EUOGZ
+=4p8E
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/spec/schleuder.yml b/spec/schleuder.yml
index 6fc619b..abb0e30 100644
--- a/spec/schleuder.yml
+++ b/spec/schleuder.yml
@@ -6,3 +6,4 @@ lists_dir: /tmp/schleuder-test/
 listlogs_dir: /tmp/schleuder-test/
 smtp_settings:
   port: 2523
+keyserver: hkp://127.0.0.1:11371
diff --git a/spec/schleuder/integration/cli_spec.rb b/spec/schleuder/integration/cli_spec.rb
index 664d86a..8477481 100644
--- a/spec/schleuder/integration/cli_spec.rb
+++ b/spec/schleuder/integration/cli_spec.rb
@@ -87,4 +87,43 @@ describe 'cli' do
       expect(subscription_emails).to eq ['schleuder2 at example.org']
     end
   end
+
+  context '#refresh_keys' do
+    it 'updates one key from the keyserver' do
+      list = create(:list)
+      list.subscribe("admin at example.org", nil, true)
+      list.import_key(File.read("spec/fixtures/expired_key.txt"))
+
+      with_sks_mock do
+        Cli.new.refresh_keys
+      end
+      mail = Mail::TestMailer.deliveries.first
+
+      expect(Mail::TestMailer.deliveries.length).to eq 1
+      expect(mail.to_s).to include("Refreshing all keys from the keyring of list #{list.email} resulted in this")
+      expect(mail.to_s).to include("98769E8A1091F36BD88403ECF71A3F8412D83889 was updated (new signatures)")
+
+      teardown_list_and_mailer(list)
+    end
+
+    it 'reports errors from refreshing keys' do
+      list = create(:list)
+      list.subscribe("admin at example.org", nil, true)
+      list.import_key(File.read("spec/fixtures/expired_key.txt"))
+
+      Cli.new.refresh_keys
+      mail = Mail::TestMailer.deliveries.first
+
+      expect(Mail::TestMailer.deliveries.length).to eq 1
+      expect(mail.to_s).to include("Refreshing all keys from the keyring of list #{list.email} resulted in this")
+      if GPGME::Ctx.sufficient_gpg_version?('2.1')
+        expect(mail.to_s).to include("keyserver refresh failed: No keyserver available")
+      else
+        # The wording differs slightly among versions.
+        expect(mail.to_s).to match(/gpgkeys: .* error .* connect/)
+      end
+
+      teardown_list_and_mailer(list)
+    end
+  end
 end
diff --git a/spec/schleuder/integration/keywords_spec.rb b/spec/schleuder/integration/keywords_spec.rb
new file mode 100644
index 0000000..703b34b
--- /dev/null
+++ b/spec/schleuder/integration/keywords_spec.rb
@@ -0,0 +1,269 @@
+require "spec_helper"
+
+describe "user sends keyword" do
+  it "x-subscribe without attributes" 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-SUBSCRIBE: test at example.org"
+    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 = raw.setup(list.request_address, list)
+    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*$/)
+    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 be_blank
+    expect(subscription.admin).to eql(false)
+    expect(subscription.delivery_enabled).to eql(true)
+
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-subscribe with attributes" 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-SUBSCRIBE: test at example.org 0x#{list.fingerprint} true false"
+    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 = raw.setup(list.request_address, list)
+    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? false")
+
+    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(false)
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-add-key with inline key-material" 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)
+    keymaterial = File.read('spec/fixtures/example_key.txt')
+    mail.body = "x-listname: #{list.email}\nX-ADD-KEY:\n#{keymaterial}"
+    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 = raw.setup(list.request_address, list)
+
+    expect(message.to).to eql(['schleuder at example.org'])
+    expect(message.to_s).to include("C4D60F8833789C7CAA44496FD3FFA6613AB10ECE: imported")
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-add-key with attached key-material" 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-ADD-KEY:"
+    mail.add_file('spec/fixtures/example_key.txt')
+    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 = raw.setup(list.request_address, list)
+
+    expect(message.to).to eql(['schleuder at example.org'])
+    expect(message.to_s).to include("C4D60F8833789C7CAA44496FD3FFA6613AB10ECE: imported")
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-resend" 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!\n"
+    mail.body = "x-listname: #{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
+    raw = Mail::TestMailer.deliveries.last
+    message = raw.setup(list.email, list)
+
+    expect(message.to).to eql(['schleuder at example.org'])
+    expect(message.to_s).to include("Resent: Unencrypted to someone at example.org")
+    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_body).to eql(content_body + list.public_footer.to_s)
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-resend without x-listname" 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"
+    mail.body = "X-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
+    message = raw.setup(list.email, list)
+
+    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.")
+
+    teardown_list_and_mailer(list)
+  end
+
+  it "x-resend with wrong x-listname" 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"
+    mail.body = "x-listname: somethingelse at example.org\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
+    message = raw.setup(list.email, list)
+
+    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 a wrong X-LISTNAME-keyword. The value of that keyword muss match the email address of this list.")
+
+    teardown_list_and_mailer(list)
+  end
+end
+
+
+
diff --git a/spec/schleuder/runner_spec.rb b/spec/schleuder/runner_spec.rb
index 45db7d9..e229ccc 100644
--- a/spec/schleuder/runner_spec.rb
+++ b/spec/schleuder/runner_spec.rb
@@ -128,9 +128,40 @@ describe Schleuder::Runner do
       end
     end
 
-    def teardown_list_and_mailer(list)
-      FileUtils.rm_rf(list.listdir)
-      Mail::TestMailer.deliveries.clear
+    it "delivers a signed error message if a subscription's key is expired on a encrypted-only list" do
+        list = create(:list, send_encrypted_only: true)
+        list.subscribe("admin at example.org", nil, true, false)
+        list.subscribe("expired at example.org", '98769E8A1091F36BD88403ECF71A3F8412D83889')
+        key = File.read("spec/fixtures/expired_key.txt")
+        list.import_key(key)
+        mail = File.read("spec/fixtures/mails/plain/thunderbird.eml")
+
+        Schleuder::Runner.new().run(mail, list.email)
+        message = Mail::TestMailer.deliveries.first
+        verified = message.verify
+        signature_fingerprints = verified.signatures.map(&:fpr)
+
+        expect(Mail::TestMailer.deliveries.size).to eq 1
+        expect(message.to).to include('expired at example.org')
+        expect(message.to_s).to include("You missed an email from ")
+        expect(signature_fingerprints).to eq([list.fingerprint])
+
+        teardown_list_and_mailer(list)
     end
+
+    it "delivers a signed error message if a subscription's key is not available on a encrypted-only list" do
+        list = create(:list, send_encrypted_only: true)
+        list.subscribe("admin at example.org", 'AAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDEEEEEE', true)
+        mail = File.read("spec/fixtures/mails/plain/thunderbird.eml")
+
+        Schleuder::Runner.new().run(mail, list.email)
+        message = Mail::TestMailer.deliveries.first
+
+        expect(message.to).to eq ['admin at example.org']
+        expect(message.to_s).to include("You missed an email from #{list.email} ")
+
+        teardown_list_and_mailer(list)
+    end
+
   end
 end
diff --git a/spec/schleuder/unit/list_spec.rb b/spec/schleuder/unit/list_spec.rb
index 7fe1c74..040db80 100644
--- a/spec/schleuder/unit/list_spec.rb
+++ b/spec/schleuder/unit/list_spec.rb
@@ -92,7 +92,7 @@ describe Schleuder::List do
     list = build(:list, fingerprint: "&$$$$67923AAA")
 
     expect(list).not_to be_valid
-    expect(list.errors.messages[:fingerprint]).to include("is not a valid fingerprint")
+    expect(list.errors.messages[:fingerprint]).to include("is not a valid OpenPGP-fingerprint")
   end
 
   BOOLEAN_LIST_ATTRIBUTES.each do |list_attribute|
diff --git a/spec/schleuder/unit/subscription_spec.rb b/spec/schleuder/unit/subscription_spec.rb
index 72ee3c8..0a5d608 100644
--- a/spec/schleuder/unit/subscription_spec.rb
+++ b/spec/schleuder/unit/subscription_spec.rb
@@ -72,7 +72,7 @@ describe Schleuder::Subscription do
     subscription = build(:subscription, fingerprint: "&$$$$123AAA")
 
     expect(subscription).not_to be_valid
-    expect(subscription.errors.messages[:fingerprint]).to include("is not a valid fingerprint")
+    expect(subscription.errors.messages[:fingerprint]).to include("is not a valid OpenPGP-fingerprint")
   end
 
   BOOLEAN_SUBSCRIPTION_ATTRIBUTES.each do |subscription_attribute|
diff --git a/spec/sks-mock.rb b/spec/sks-mock.rb
new file mode 100755
index 0000000..6d7bd17
--- /dev/null
+++ b/spec/sks-mock.rb
@@ -0,0 +1,26 @@
+#!/usr/bin/env ruby
+
+require 'sinatra/base'
+
+class SksMock < Sinatra::Base
+  set :environment, :production
+  set :port, 11371
+  set :bind, '127.0.0.1'
+  set :logging, true
+
+  get '/status' do
+    'ok'
+  end
+
+  get '/pks/lookup' do
+    case params['search']
+    when '0x98769E8A1091F36BD88403ECF71A3F8412D83889'
+      File.read('spec/fixtures/expired_key_extended.txt')
+    else
+      404
+    end
+  end
+
+  # Run this class as application
+  run!
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 2024bcd..ec293f8 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -4,6 +4,7 @@ ENV["SCHLEUDER_LIST_DEFAULTS"] = "etc/list-defaults.yml"
 require 'bundler/setup'
 Bundler.setup
 require 'schleuder'
+require 'schleuder/cli'
 require 'database_cleaner'
 require 'factory_girl'
 
@@ -54,6 +55,14 @@ RSpec.configure do |config|
     File.join(Conf.lists_dir, 'smtp-daemon-output')
   end
 
+  def with_sks_mock
+    pid = Process.spawn('spec/sks-mock.rb', [:out, :err] => ["/tmp/sks-mock.log", 'w'])
+    sleep 1
+    yield
+    Process.kill 'TERM', pid
+    Process.wait pid
+  end
+
   def start_smtp_daemon
     if ! File.directory?(smtp_daemon_outputdir)
       FileUtils.mkdir_p(smtp_daemon_outputdir)
@@ -88,4 +97,9 @@ RSpec.configure do |config|
   Mail.defaults do
     delivery_method :test
   end
+  
+  def teardown_list_and_mailer(list)
+    FileUtils.rm_rf(list.listdir)
+    Mail::TestMailer.deliveries.clear
+  end
 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