[DRE-commits] [schleuder] 01/52: Import Upstream version 3.0.0~beta7

Georg Faerber georg-alioth-guest at moszumanska.debian.org
Mon Feb 6 11:21:18 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 cebcaab04bc313fc86e7eaa5674fbd83706ec944
Author: Georg Faerber <georg at riseup.net>
Date:   Fri Nov 25 14:14:07 2016 +0100

    Import Upstream version 3.0.0~beta7
---
 README.md                                          | 114 +++++++
 Rakefile                                           | 101 ++++++
 bin/schleuder                                      |  12 +
 bin/schleuder-api-daemon                           | 370 +++++++++++++++++++++
 db/schema.rb                                       |  61 ++++
 etc/list-defaults.yml                              | 117 +++++++
 etc/schleuder-api-daemon.service                   |   9 +
 etc/schleuder.yml                                  |  41 +++
 lib/schleuder.rb                                   |  78 +++++
 lib/schleuder/cli.rb                               | 248 ++++++++++++++
 lib/schleuder/cli/cert.rb                          |  19 ++
 lib/schleuder/cli/schleuder_cert_manager.rb        |  85 +++++
 lib/schleuder/cli/subcommand_fix.rb                |  11 +
 lib/schleuder/conf.rb                              |  97 ++++++
 lib/schleuder/errors/active_model_error.rb         |  15 +
 lib/schleuder/errors/base.rb                       |  17 +
 lib/schleuder/errors/decryption_failed.rb          |  16 +
 lib/schleuder/errors/file_not_found.rb             |  14 +
 lib/schleuder/errors/invalid_listname.rb           |  13 +
 lib/schleuder/errors/key_adduid_failed.rb          |  13 +
 lib/schleuder/errors/key_generation_failed.rb      |  16 +
 lib/schleuder/errors/keyword_admin_only.rb         |  13 +
 lib/schleuder/errors/list_exists.rb                |  13 +
 lib/schleuder/errors/list_not_found.rb             |  14 +
 lib/schleuder/errors/list_property_missing.rb      |  13 +
 lib/schleuder/errors/listdir_problem.rb            |  16 +
 .../errors/loading_list_settings_failed.rb         |  14 +
 lib/schleuder/errors/message_empty.rb              |  14 +
 lib/schleuder/errors/message_not_from_admin.rb     |  13 +
 .../errors/message_sender_not_subscribed.rb        |  13 +
 lib/schleuder/errors/message_too_big.rb            |  14 +
 lib/schleuder/errors/message_unauthenticated.rb    |  13 +
 lib/schleuder/errors/message_unencrypted.rb        |  13 +
 lib/schleuder/errors/message_unsigned.rb           |  13 +
 lib/schleuder/errors/standard_error.rb             |   5 +
 lib/schleuder/errors/too_many_keys.rb              |  17 +
 lib/schleuder/errors/unknown_list_option.rb        |  14 +
 lib/schleuder/filters/auth_filter.rb               |  39 +++
 lib/schleuder/filters/bounces_filter.rb            |  12 +
 lib/schleuder/filters/forward_filter.rb            |  17 +
 lib/schleuder/filters/forward_incoming.rb          |  13 +
 lib/schleuder/filters/max_message_size.rb          |  14 +
 lib/schleuder/filters/request_filter.rb            |  22 ++
 lib/schleuder/filters/send_key_filter.rb           |  27 ++
 lib/schleuder/filters_runner.rb                    |  83 +++++
 lib/schleuder/gpgme/ctx.rb                         |  39 +++
 lib/schleuder/gpgme/import_status.rb               |  27 ++
 lib/schleuder/gpgme/key.rb                         |  44 +++
 lib/schleuder/gpgme/sub_key.rb                     |  13 +
 lib/schleuder/list.rb                              | 295 ++++++++++++++++
 lib/schleuder/list_builder.rb                      | 152 +++++++++
 lib/schleuder/listlogger.rb                        |  30 ++
 lib/schleuder/logger.rb                            |  23 ++
 lib/schleuder/logger_notifications.rb              |  50 +++
 lib/schleuder/mail/message.rb                      | 302 +++++++++++++++++
 lib/schleuder/plugins/foo.rb                       |   8 +
 lib/schleuder/plugins/key_management.rb            |  51 +++
 lib/schleuder/plugins/resend.rb                    |  74 +++++
 lib/schleuder/plugins/sign_this.rb                 |  52 +++
 lib/schleuder/plugins/subscription_management.rb   | 113 +++++++
 lib/schleuder/plugins_runner.rb                    |  60 ++++
 lib/schleuder/runner.rb                            | 127 +++++++
 lib/schleuder/subscription.rb                      |  83 +++++
 lib/schleuder/validators/boolean_validator.rb      |   7 +
 lib/schleuder/validators/email_validator.rb        |   7 +
 lib/schleuder/validators/fingerprint_validator.rb  |   7 +
 .../validators/greater_than_zero_validator.rb      |   7 +
 .../validators/no_line_breaks_validator.rb         |   7 +
 lib/schleuder/version.rb                           |   3 +
 locales/de.yml                                     | 119 +++++++
 locales/en.yml                                     | 119 +++++++
 schleuder.gemspec                                  |  67 ++++
 72 files changed, 3792 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..91f8561
--- /dev/null
+++ b/README.md
@@ -0,0 +1,114 @@
+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 [webschleuder](https://git.codecoop.org/schleuder/webschleuder3).
+
+For more details see <https://schleuder.nadir.org/docs/>.
+
+Requirements
+------------
+* ruby  >=2.1
+* gnupg >=2.0 (if possible use >= 2.1.14)
+* gpgme
+* sqlite3
+
+On Debian-based systems, install these via
+
+    apt-get install ruby2.1-dev gnupg2 libgpgme11-dev libsqlite3-dev
+
+
+We **recommend** to also run a random number generator like [haveged](http://www.issihosts.com/haveged/). This ensures Schleuder won't be blocked by lacking entropy, which otherwise might happen especially during key generation.
+
+On Debian based systems, install it via
+
+    apt-get install haveged
+
+
+Additionally these **rubygems** are required (will be installed automatically unless present):
+
+* rake
+* active_record
+* sqlite3
+* thor
+* mail-gpg
+* sinatra
+* sinatra-contrib
+
+
+Installing Schleuder
+------------
+
+1. Download [the gem](https://git.codecoop.org/schleuder/schleuder3/raw/master/gems/schleuder-3.0.0.beta6.gem) and [the OpenPGP-signature](https://git.codecoop.org/schleuder/schleuder3/raw/master/gems/schleuder-3.0.0.beta6.gem.sig) and verify:
+   ```
+   gpg --recv-key 0x75C9B62688F93AC6574BDE7ED8A6EF816E1C6F25
+   gpg --verify schleuder-3.0.0.beta6.gem.sig
+   ```
+
+2. If all went well install the gem:
+   ```
+   gem install schleuder-3.0.0.beta6.gem
+   ```
+
+3. Set up schleuder:
+  ```
+  schleuder install
+  ```
+  This creates neccessary directories, copies example configs, etc. If you see errors about missing write permissions please follow the advice given.
+
+
+Command line usage
+-----------------
+
+See `schleuder help`.
+
+E.g.:
+
+    Commands:
+      schleuder check_keys                    # Check all lists for unusable or expiring keys and send the results to the list-admins. (This is supposed...
+      schleuder help [COMMAND]                # Describe available commands or one specific command
+      schleuder install                       # Set up Schleuder initially. Create folders, copy files, fill the database, etc.
+      schleuder version                       # Show version of schleuder
+      schleuder work list at hostname < message  # Run a message through a list.
+
+List administration
+-------------------
+
+You probably want to install
+[schleuder-conf](https://git.codecoop.org/schleuder/schleuder-conf), too.
+Otherwise you'd need to edit the database-records manually to change
+list-settings, subscribe addresses, etc.
+
+Optionally consider installing
+[webschleuder](https://git.codecoop.org/schleuder/webschleuder3), the web
+interface for schleuder.
+
+
+
+Todo
+----
+
+See <https://git.codecoop.org/schleuder/schleuder3/issues>.
+
+Testing
+-------
+
+    SCHLEUDER_ENV=test SCHLEUDER_CONFIG=spec/schleuder.yml bundle exec rake db:create db:schema:load
+    bundle exec rspec
+
+
+Contributing
+------------
+
+To contribute please follow this workflow:
+
+1. Talk to us! E.g. create an issue about your idea or problem.
+2. Fork the repository and work in a meaningful named branch that is based off of our "master".
+3. Commit in rather small chunks but don't split depending code across commits. Please write sensible commit messages.
+4. If in doubt request feedback from us!
+5. When finished create a merge request.
+
+
+Thank you for your interest!
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..fbdd31f
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,101 @@
+project = 'schleuder'
+require_relative "lib/#{project}.rb"
+
+version = Schleuder::VERSION
+tagname = "#{project}-#{version}"
+gpguid = 'schleuder at nadir.org'
+tarball = "#{tagname}.tar.gz"
+
+load "active_record/railties/databases.rake"
+
+# Configure ActiveRecord
+ActiveRecord::Tasks::DatabaseTasks.tap do |config|
+  config.root = File.dirname(__FILE__)
+  config.db_dir = 'db'
+  config.migrations_paths = ['db/migrate']
+  config.env = ENV['SCHLEUDER_ENV']
+  config.database_configuration = Schleuder::Conf.databases
+end
+
+# ActiveRecord requires this task to be present
+Rake::Task.define_task("db:environment")
+
+task :console do
+  exec "irb -r #{File.dirname(__FILE__)}/lib/schleuder.rb"
+end
+
+desc 'Release a new version of schleuder.'
+task :release => [:git_tag, :gem, :publish_gem, :tarball, :wiki]
+
+task :gem => :check_version
+task :git_tag => :check_version
+task :tarball => :check_version
+
+desc "Build new version: git-tag and gem-file"
+task :new_version => [:gem, :edit_readme, :git_commit_version, :git_tag] do
+end
+
+desc "Edit README"
+task :edit_readme do
+  puts "Please edit the README to refer to version #{version}"
+  if system('gvim -f README.md')
+    `git add README.md`
+  else
+    exit 1
+  end
+end
+
+desc 'git-tag HEAD as new version'
+task :git_tag do
+  `git tag -u #{gpguid} -s -m "Version #{version}" #{tagname}`
+end
+
+desc "Commit changes as new version"
+task :git_commit_version do
+  `git add lib/#{project}/version.rb`
+  `git commit -m "Version #{version} (README, gems)"`
+end
+
+desc 'Build, sign and commit a gem-file.'
+task :gem do
+  gemfile = "#{tagname}.gem"
+  `gem build #{project}.gemspec`
+  `mv -iv #{gemfile} gems/`
+  `cd gems && gpg -u #{gpguid} -b #{gemfile}`
+  `git add gems/#{gemfile}*`
+end
+
+desc 'Publish gem-file to rubygems.org'
+task :publish_gem do
+  `gem push #{tagname}.gem`
+end
+
+desc 'Build and sign a tarball'
+task :tarball do
+  `git archive --format tar.gz --prefix "#{tagname}/" -o #{tarball} #{tagname}`
+  `gpg -u schleuder2 at nadir.org --detach-sign #{tarball}`
+end
+
+desc 'Describe manual wiki-related release-tasks'
+task :wiki do
+  puts "Please update the website:
+  * Upload tarball+signature.
+  * Edit download- and changelog-pages.
+  * Publish release-announcement.
+"
+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)
+    $stderr.puts "Warning: Tag '#{tagname}' already exists. Did you forget to update #{project}/version.rb?"
+    $stderr.print "Continue? [yN] "
+    if $stdin.gets.match(/^y/i)
+      `git tag -d #{tagname}`
+    else
+      exit 1
+    end
+  end
+end
+
diff --git a/bin/schleuder b/bin/schleuder
new file mode 100755
index 0000000..342dd2e
--- /dev/null
+++ b/bin/schleuder
@@ -0,0 +1,12 @@
+#!/usr/bin/env ruby
+
+trap("INT") { exit 1 }
+
+
+begin
+  require_relative '../lib/schleuder/cli'
+  Schleuder::Cli.start
+rescue => exc
+  $stderr.puts exc.to_s
+  exit 1
+end
diff --git a/bin/schleuder-api-daemon b/bin/schleuder-api-daemon
new file mode 100755
index 0000000..150722c
--- /dev/null
+++ b/bin/schleuder-api-daemon
@@ -0,0 +1,370 @@
+#!/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'
+
+TLS_CERT = Conf.api['tls_cert_file']
+TLS_KEY = Conf.api['tls_key_file']
+
+class SchleuderApiDaemon < Sinatra::Base
+  register Sinatra::Namespace
+  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
+
+  # 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
+
+  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 Array
+               obj_or_msg.join("\n")
+             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.trust
+      }
+    end
+  end
+
+  namespace '/lists' do
+    get '.json' do
+      json List.all, include: :subscriptions
+    end
+
+    post '.json' do
+      listname = parsed_body['email']
+      adminaddress = parsed_body['adminaddress']
+      adminkey = parsed_body['adminkey']
+      list, messages = ListBuilder.new({email: listname}, 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
+
+    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 '/: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
+
+    get '/check_keys.json' do
+      json result: list.check_keys
+    end
+  end
+
+  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
+    end
+  end
+
+  # Run this class as application
+  run!
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000..c084225
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,61 @@
+# encoding: UTF-8
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 20160501172700) do
+
+  create_table "lists", force: :cascade do |t|
+    t.datetime "created_at"
+    t.datetime "updated_at"
+    t.string   "email",                                       limit: 255
+    t.string   "fingerprint",                                 limit: 255
+    t.string   "log_level",                                   limit: 255, default: "warn"
+    t.string   "subject_prefix",                              limit: 255, default: ""
+    t.string   "subject_prefix_in",                           limit: 255, default: ""
+    t.string   "subject_prefix_out",                          limit: 255, default: ""
+    t.string   "openpgp_header_preference",                   limit: 255, 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: "[\"subscribe\", \"unsubscribe\", \"delete-key\"]"
+    t.text     "keywords_admin_notify",                                   default: "[\"add-key\"]"
+    t.boolean  "send_encrypted_only",                                     default: true
+    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
+    t.string   "language",                                    limit: 255, default: "en"
+    t.boolean  "forward_all_incoming_to_admins",                          default: false
+    t.integer  "logfiles_to_keep",                                        default: 2
+  end
+
+  create_table "subscriptions", force: :cascade do |t|
+    t.integer  "list_id"
+    t.string   "email",            limit: 255
+    t.string   "fingerprint",      limit: 255
+    t.boolean  "admin",                        default: false
+    t.boolean  "delivery_enabled",             default: true
+    t.datetime "created_at"
+    t.datetime "updated_at"
+  end
+
+  add_index "subscriptions", ["email", "list_id"], name: "index_subscriptions_on_email_and_list_id", unique: true
+  add_index "subscriptions", ["list_id"], name: "index_subscriptions_on_list_id"
+
+end
diff --git a/etc/list-defaults.yml b/etc/list-defaults.yml
new file mode 100644
index 0000000..2e4321a
--- /dev/null
+++ b/etc/list-defaults.yml
@@ -0,0 +1,117 @@
+# Setting default values for newly generated lists. Once a list is created it
+# is not affected by these settings but has its own set of options in the
+# database.
+#
+# The configuration format is yaml (http://www.yaml.org).
+#
+# Options are listed with the behaviour encoded in the database schema.
+
+# Only send out enrypted emails?
+send_encrypted_only: true
+
+# Allow only encrypted emails? If true, any other email will be bounced.
+receive_encrypted_only: false
+
+# Allow only emails that are validly signed? If true, any other email will be
+# bounced.
+receive_signed_only: false
+
+# Allow only emails that are validly signed by a subscriber's key?  If true,
+# any other email will be bounced.
+receive_authenticated_only: false
+
+# Allow only emails being sent from subscribed addresses? If true, any other
+# email will be bounced.
+# NOTE: This is a very weak restriction mechanism on which you should not rely,
+#       as sending addresses can easily be faked! We recommend you to rather
+#       rely on the `receive_authenticated_only` option. Setting the
+#       `receive_authenticated_only` option to true, will authenticated senders
+#       based on the signature on the mail, which is the strongest
+#       authentication mechanism you can get.
+#       This option could be useful, if you would like to have a closed
+#       mailinglist, but could not yet get all subscribers to properly use GPG.
+receive_from_subscribed_emailaddresses_only: false
+
+# Allow only emails that are validly signed by a list-admin's key.
+# This is useful for newsletters, announce or notification lists
+receive_admin_only: false
+
+# Which headers to include from the original mail.
+headers_to_meta:
+- from
+- to
+- cc
+- date
+
+# Preserve the Message-IDs (In-Reply-To, References) from the incoming email.
+# This setting can lead to information leakage, as replies are connectable
+# and a thread of (encrypted) messages can be built by an eavesdropper.
+keep_msgid: true
+
+# Which keywords ("email-commands") should be restricted to list-admins?
+keywords_admin_only:
+- subscribe
+- unsubscribe
+- delete-key
+
+# For which keywords should the list-admins receive a notice whenever it
+# triggers a command.
+keywords_admin_notify: 
+- add-key
+
+# Public footer to append each email that is sent to non-subscribed addresses.
+public_footer:
+
+# Prefix to be inserted into the subject of every email that is validly signed
+# by a subscribed address.
+subject_prefix:
+
+# Prefix to be inserted into the subject of every email that is *not* validly
+# signed by a subscribed address.
+subject_prefix_in:
+
+# Prefix to be inserted into the subject of every email that has been
+# resent to a non-subscribed address.
+subject_prefix_out:
+
+# Drop any bounces (incoming emails not passing the receive_*_only-rules)?
+bounces_drop_all: false
+
+# Drop bounces if they match one of these headers. Must be a hash, keys
+# and values are case insensitive.
+bounces_drop_on_headers: 
+  x-spam-flag: yes
+
+# Send a notice to the list-admins whenever an email is bounced or dropped?
+bounces_notify_admins: true
+
+# Include RFC-compliant List-* Headers into emails?
+include_list_headers: true
+
+# Include OpenPGP-Header into emails?
+include_openpgp_header: true
+
+# Prefered way to receive emails to note in OpenPGP-Header
+# ('sign'|'encrypt'|'signencrypt'|'unprotected'|'none')
+openpgp_header_preference: signencrypt
+
+# Maximum size of emails allowed on the list, in kilobyte. All others will be
+# bounced.
+max_message_size_kb: 10240
+
+# How verbose to log on the list-level (Notifications will be sent to
+# list-admins)? Error, warn, info, or debug.
+log_level: warn
+
+# How many logfiles to keep, including the current one.
+# Logfiles are rotated daily, so 2 means: delete logfiles older than
+# yesterday.  Values lower than 1 are ignored.
+logfiles_to_keep: 2
+
+# Which language to use for automated replies, error-messages, etc.
+# Available: en, de.
+language: en
+
+# Forward a raw copy of all incoming emails to the list-admins?
+# Mainly useful for debugging.
+forward_all_incoming_to_admins: false
diff --git a/etc/schleuder-api-daemon.service b/etc/schleuder-api-daemon.service
new file mode 100644
index 0000000..3838e0b
--- /dev/null
+++ b/etc/schleuder-api-daemon.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=Schleuder API daemon
+
+[Service]
+ExecStart=/usr/local/bin/schleuder-api-daemon
+User=schleuder
+
+[Install]
+WantedBy=multi-user.target
diff --git a/etc/schleuder.yml b/etc/schleuder.yml
new file mode 100644
index 0000000..0a482eb
--- /dev/null
+++ b/etc/schleuder.yml
@@ -0,0 +1,41 @@
+# Where are the list-directories stored (contain log-files and GnuPG-keyrings).
+lists_dir: /var/schleuder/lists
+
+# Schleuder reads plugins also from this directory.
+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
+
+# For these options see documentation for ActionMailer::smtp_settings, e.g. <http://api.rubyonrails.org/classes/ActionMailer/Base.html>.
+smtp_settings:
+  address: localhost
+  port: 25
+  #domain:
+  #enable_starttls_auto:
+  #openssl_verify_mode:
+  #authentication:
+  #user_name:
+  #password:
+
+# The database to use. Unless you want to run the tests you only need the `production`-section.
+database:
+  production:
+    adapter:  'sqlite3'
+    database: /var/schleuder/db.sqlite
+
+# 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
+  # List of api_keys to allow access to the API.
+  # Example:
+  # valid_api_keys:
+  #   - abcdef...
+  #   - zyxwvu...
+  valid_api_keys:
diff --git a/lib/schleuder.rb b/lib/schleuder.rb
new file mode 100644
index 0000000..cfb452d
--- /dev/null
+++ b/lib/schleuder.rb
@@ -0,0 +1,78 @@
+# Stdlib
+require 'fileutils'
+require 'singleton'
+require 'yaml'
+require 'pathname'
+require 'syslog/logger'
+require 'logger'
+require 'open3'
+
+# Require mandatory libs. The database-layer-lib is required below.
+require 'mail-gpg'
+require 'active_record'
+
+# An extra from mail-gpg
+require 'hkp'
+
+# Load schleuder
+rootdir = Pathname.new(__FILE__).dirname.dirname.realpath
+$:.unshift File.join(rootdir, 'lib')
+
+# Monkeypatches
+require 'schleuder/mail/message.rb'
+require 'schleuder/gpgme/import_status.rb'
+require 'schleuder/gpgme/key.rb'
+require 'schleuder/gpgme/sub_key.rb'
+require 'schleuder/gpgme/ctx.rb'
+
+# The Code[tm]
+require 'schleuder/errors/base'
+Dir[rootdir.to_s + "/lib/schleuder/errors/*.rb"].each do |file|
+  require file
+end
+# Load schleuder/conf before the other classes, it defines constants!
+require 'schleuder/conf'
+require 'schleuder/version'
+require 'schleuder/logger_notifications'
+require 'schleuder/logger'
+require 'schleuder/listlogger'
+require 'schleuder/plugins_runner'
+Dir[rootdir.to_s + "/lib/schleuder/plugins/*.rb"].each do |file|
+  require file
+end
+require 'schleuder/filters_runner'
+Dir[rootdir.to_s + "/lib/schleuder/filters/*.rb"].each do |file|
+  require file
+end
+Dir[rootdir.to_s + "/lib/schleuder/validators/*.rb"].each do |file|
+  require file
+end
+require 'schleuder/runner'
+require 'schleuder/list'
+require 'schleuder/list_builder'
+require 'schleuder/subscription'
+
+# Setup
+ENV["SCHLEUDER_CONFIG"] ||= '/etc/schleuder/schleuder.yml'
+ENV["SCHLEUDER_LIST_DEFAULTS"] ||= '/etc/schleuder/list-defaults.yml'
+ENV["SCHLEUDER_ENV"] ||= 'production'
+ENV["SCHLEUDER_ROOT"] = rootdir.to_s
+
+GPGME::Ctx.check_gpg_version
+
+# Require lib for database specified in config.
+require Schleuder::Conf.database['adapter']
+
+# TODO: Test if database is writable if sqlite.
+ActiveRecord::Base.establish_connection(Schleuder::Conf.databases[ENV["SCHLEUDER_ENV"]])
+ActiveRecord::Base.logger = Schleuder.logger
+
+Mail.defaults do
+  delivery_method :smtp, Schleuder::Conf.smtp_settings.symbolize_keys
+end
+
+I18n.load_path += Dir[rootdir.to_s + "/locales/*.yml"]
+I18n.enforce_available_locales = true
+I18n.default_locale = :en
+
+include Schleuder
diff --git a/lib/schleuder/cli.rb b/lib/schleuder/cli.rb
new file mode 100644
index 0000000..6bd7346
--- /dev/null
+++ b/lib/schleuder/cli.rb
@@ -0,0 +1,248 @@
+require 'thor'
+require 'yaml'
+require 'gpgme'
+
+require_relative '../schleuder'
+require 'schleuder/cli/subcommand_fix'
+require 'schleuder/cli/schleuder_cert_manager'
+require 'schleuder/cli/cert'
+
+module Schleuder
+  class Cli < Thor
+
+    register(Cert,
+             'cert',
+             'cert ...',
+             'Generate TLS-certificate and show fingerprint')
+
+    map '-v' => :version
+    map '--version' => :version
+    desc 'version', 'Show version of schleuder'
+    def version
+      say Schleuder::VERSION
+    end
+
+    desc 'new_api_key', 'Generate a new API key to be used by a client.'
+    def new_api_key
+      require 'securerandom'
+      puts SecureRandom.hex(32)
+    end
+
+    desc 'work list at hostname < message', 'Run a message through a list.'
+    def work(listname)
+      message  = STDIN.read
+
+      error = Schleuder::Runner.new.run(message, listname)
+      if error.kind_of?(StandardError)
+        fatal error
+      end
+    rescue => exc
+      begin
+        Schleuder.logger.fatal(exc.message_with_backtrace, message)
+        say I18n.t('errors.fatalerror')
+      rescue => e
+        # Give users a clue what to do in case everything blows up.
+        # As apparently even the logging raised exceptions we can't even store
+        # any information in the logs.
+        fatal "A serious, unhandleable error happened. Please contact the administrators of this system or service and provide them with the following information:\n\n#{e.message}"
+      end
+      exit 1
+    end
+
+    desc 'check_keys', 'Check all lists for unusable or expiring keys and send the results to the list-admins. (This is supposed to be run from cron weekly.)'
+    def check_keys(listname=nil)
+      Schleuder::List.all.each do |list|
+        I18n.locale = list.language
+
+        text = list.check_keys
+
+        if text && ! text.empty?
+          msg = "#{I18n.t('check_keys_intro', email: list.email)}\n\n#{text}"
+          list.logger.notify_admin(msg, nil, I18n.t('check_keys'))
+        end
+      end
+    end
+
+    desc 'install', "Set-up or update Schleuder environment (create folders, copy files, fill the database)."
+    def install
+      %w[/var/schleuder/lists /etc/schleuder].each do |dir|
+        dir = Pathname.new(dir)
+        if ! dir.exist?
+          if dir.dirname.writable?
+            dir.mkpath
+          else
+            fatal "Cannot create required directory due to lacking write permissions, please create manually and then run this command again:\n#{dir}"
+          end
+        end
+      end
+
+      Pathname.glob(Pathname.new(root_dir).join("etc").join("*.yml")).each do |file|
+        target = Pathname.new("/etc/schleuder/").join(file.basename)
+        if ! target.exist?
+          if target.dirname.writable?
+            FileUtils.cp file, target
+          else
+            fatal "Cannot copy default config file due to lacking write permissions, please copy manually and then run this command again:\n#{file.realpath} → #{target}"
+          end
+        end
+      end
+
+      if ActiveRecord::SchemaMigration.table_exists?
+        say `cd #{root_dir} && rake db:migrate`
+      else
+        say `cd #{root_dir} && rake db:schema:load`
+        say "NOTE: The database was prepared using sqlite. If you prefer to use a different DBMS please edit the 'database'-section in /etc/schleuder/schleuder.yml, create the database, install the corresponding ruby-library (e.g. `gem install mysql`) and run this current command again"
+      end
+
+      say "Schleuder has been set up. You can now create a new list using `schleuder-conf`.\nWe hope you enjoy!"
+    rescue => exc
+      fatal exc.message
+    end
+
+    desc 'migrate-v2-list /path/to/listdir', 'Migrate list from v2.2 to v3.'
+    def migrate_v2_list(path)
+      dir = Pathname.new(path)
+      if ! dir.readable? || ! dir.directory?
+        fatal "Not a readable directory: `#{path}`."
+      end
+
+      %w[list.conf members.conf pubring.gpg].each do |file|
+        if ! (dir + file).exist?
+          fatal "Not a complete schleuder v2.2 listdir: missing #{file}"
+        end
+      end
+
+      conf = YAML.load(File.read(dir + 'list.conf'))
+      if conf.nil? || conf.empty?
+        fatal "list.conf is blank"
+      end
+      listname = conf['myaddr']
+      if listname.nil? || listname.empty?
+        fatal "myaddr is blank in list.conf"
+      end
+
+      # Identify list-fingerprint.
+      ENV['GNUPGHOME'] = dir.to_s
+      # Save all the keys for later import, we shouldn't change ENV['GNUPGHOME'] later.
+      #allkeys = GPGME::Key.find(:public, '')
+      listkey = GPGME::Key.find(:public, "<#{listname}>")
+      if listkey.size == 1
+        fingerprint = listkey.first.fingerprint
+      else
+        fingerprint = nil
+        error 'Failed to identify fingerprint of GnuPG key for list, you must set it manually to make the list operational!'
+      end
+
+      # Create list.
+      # TODO: Check for errors!
+      list, messages = Schleuder::ListBuilder.new({email: listname, fingerprint: fingerprint}).run
+
+      # Set list-options.
+      List.configurable_attributes.each do |option|
+        option = option.to_s
+        if conf[option]
+          value = if option.match(/^keywords_/)
+                    filter_keywords(conf[option])
+                  else
+                    conf[option]
+                  end
+          list.set_attribute(option, value)
+        end
+      end
+
+      # Set changed options.
+      { 'prefix' => 'subject_prefix',
+        'prefix_in' => 'subject_prefix_in',
+        'prefix_out' => 'subject_prefix_out',
+        'dump_incoming_mail' => 'forward_all_incoming_to_admins',
+        'receive_from_member_emailaddresses_only' => 'receive_from_subscribed_emailaddresses_only',
+        'bounces_notify_admin' => 'bounces_notify_admins',
+        'max_message_size' => 'max_message_size_kb'
+      }.each do |old, new|
+        if conf[old] && ! conf[old].to_s.empty?
+          list.set_attribute(new, conf[old])
+        end
+      end
+      list.save!
+
+      # Import keys
+      list.import_key(File.read(dir + 'pubring.gpg'))
+
+      # Subscribe members
+      YAML.load(File.read(dir + 'members.conf')).each do |member|
+        list.subscribe(member['email'], member['fingerprint'])
+      end
+
+      # Subscribe or flag admins
+      conf['admins'].each do |member|
+        sub = list.subscriptions.where(email: member['email']).first
+        if sub
+          sub.admin = true
+          sub.save!
+        else
+          adminfpr = member['fingerprint'] || list.keys(member['email']).first.fingerprint
+          list.subscribe(member['email'], adminfpr, true)
+        end
+      end
+
+      # Notify of removed options
+      say "Please note: the following options have been *removed*:
+* `default_mime` for lists (we only support pgp/mime for now),
+* `archive` for lists,
+* `gpg_passphrase` for lists,
+* `log_file`, `log_io`, `log_syslog` for lists (we only log to
+         syslog (before list-creation) and a file (after it) for now),
+* `mime` for subscriptions/members (we only support pgp/mime for now),
+* `send_encrypted_only` for members/subscriptions.
+
+If you really miss any of them please tell us.
+
+Please also note that the following keywords have been renamed:
+* list-members  => list-subscriptions
+* add-member    => subscribe
+* delete-member => unsubscribe
+
+Please notify the users and admins of this list of these changes.
+"
+
+      say "\nList #{listname} migrated to schleuder v3."
+      if messages.present?
+        say messages.gsub(' // ', "\n")
+      end
+    rescue => exc
+      fatal [exc, exc.backtrace.slice(0,2)].join("\n")
+    end
+
+    no_commands do
+      def fatal(msg)
+        error("Error: #{msg}")
+        exit 1
+      end
+
+      KEYWORDS = {
+        'add-member' => 'subscribe',
+        'delete-member' => 'unsubscribe',
+        'list-members' => 'list-subscriptions',
+        'subscribe' => 'subscribe',
+        'unsubscribe' => 'unsubscribe',
+        'list-subscriptions' => 'list-subscriptions',
+        'set-finterprint' => 'set-fingerprint',
+        'add-key' => 'add-key',
+        'delete-key' => 'delete-key',
+        'list-keys' => 'list-keys',
+        'get-key' => 'get-key',
+        'fetch-key' => 'fetch-key'
+      }
+
+      def filter_keywords(value)
+        Array(value).map do |keyword|
+          KEYWORDS[keyword.downcase]
+        end.compact
+      end
+
+      def root_dir
+        Pathname.new(__FILE__).dirname.dirname.dirname.realpath
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/cli/cert.rb b/lib/schleuder/cli/cert.rb
new file mode 100644
index 0000000..5974aad
--- /dev/null
+++ b/lib/schleuder/cli/cert.rb
@@ -0,0 +1,19 @@
+module Schleuder
+  class Cert < Thor
+    extend SubcommandFix
+
+    desc 'generate', 'Generate a new TLS-certificate.'
+    def generate
+      key = Conf.api['tls_key_file']
+      cert = Conf.api['tls_cert_file']
+      SchleuderCertManager.generate('schleuder', key, cert)
+    end
+
+    desc 'fingerprint', 'Show fingerprint of configured certificate.'
+    def fingerprint
+      cert = Conf.api['tls_cert_file']
+      fingerprint = SchleuderCertManager.fingerprint(cert)
+      say "Fingerprint of #{Conf.api['tls_cert_file']}: #{fingerprint}"
+    end
+  end
+end
diff --git a/lib/schleuder/cli/schleuder_cert_manager.rb b/lib/schleuder/cli/schleuder_cert_manager.rb
new file mode 100644
index 0000000..68e3fe4
--- /dev/null
+++ b/lib/schleuder/cli/schleuder_cert_manager.rb
@@ -0,0 +1,85 @@
+require 'openssl'
+require 'pathname'
+
+class SchleuderCertManager
+  def self.generate(project_name, filename_key, filename_cert)
+    keysize = 2048
+    subject = "/C=MW/O=Schleuder/OU=#{project_name}"
+    filename_key = Pathname.new(filename_key).expand_path
+    filename_cert = Pathname.new(filename_cert).expand_path
+
+    key = OpenSSL::PKey::RSA.new(keysize)
+    cert = OpenSSL::X509::Certificate.new
+    cert.subject = OpenSSL::X509::Name.parse(subject)
+    cert.issuer = cert.subject
+    cert.not_before = Time.now
+    cert.not_after = Time.now + 10 * 365 * 24 * 60 * 60
+    cert.public_key = key.public_key
+    cert.serial = 0x0
+    cert.version = 2
+
+    ef = OpenSSL::X509::ExtensionFactory.new
+    ef.subject_certificate = cert
+    ef.issuer_certificate = cert
+    cert.extensions = [
+      ef.create_extension("basicConstraints","CA:TRUE", true),
+      ef.create_extension("subjectKeyIdentifier", "hash"),
+    ]
+    cert.add_extension ef.create_extension("authorityKeyIdentifier",
+                                           "keyid:always,issuer:always")
+
+    cert.sign key, OpenSSL::Digest::SHA256.new
+
+    filename_key = prepare_writing(filename_key)
+    filename_cert = prepare_writing(filename_cert)
+
+    filename_key.open('w', 400) do |fd|
+      fd.puts key
+    end
+    puts "Private key written to: #{filename_key}"
+
+    filename_cert.open('w') do |fd|
+      fd.puts cert.to_pem
+    end
+    puts "Certificate written to: #{filename_cert}"
+
+    puts "Fingerprint of generated certificate: #{fingerprint(cert)}"
+    puts "Have this fingerprint included into the configuration-file of all clients that want to connect to your Schleuder API."
+  rescue => exc
+    error exc.message
+  end
+
+  def self.fingerprint(cert)
+    if ! cert.is_a?(OpenSSL::X509::Certificate)
+      path = Pathname.new(cert).expand_path
+      if ! path.readable?
+        error "Error: Not a readable file: #{path}"
+      end
+      cert = OpenSSL::X509::Certificate.new(path.read)
+    end
+    OpenSSL::Digest::SHA256.new(cert.to_der).to_s
+  end
+
+  def self.error(msg)
+    $stderr.puts "Error: #{msg}"
+    exit 1
+  end
+
+  def self.note(msg)
+    $stdout.puts "Note: #{msg}"
+  end
+
+  def self.prepare_writing(filename)
+    if filename.exist?
+      note "File exists: #{filename} — writing to current directory, you should move the file manually or change the configuration file."
+      if filename.basename.exist?
+        error "File exists: #{filename.basename} — (re)move it or fix previous error and try again."
+      end
+      filename = filename.basename
+    end
+    if ! filename.dirname.exist?
+      filename.dirname.mkpath
+    end
+    filename
+  end
+end
diff --git a/lib/schleuder/cli/subcommand_fix.rb b/lib/schleuder/cli/subcommand_fix.rb
new file mode 100644
index 0000000..5c01226
--- /dev/null
+++ b/lib/schleuder/cli/subcommand_fix.rb
@@ -0,0 +1,11 @@
+module Schleuder
+  module SubcommandFix
+
+    # Fixing a bug in Thor where the actual subcommand wouldn't show up
+    # with some invokations of the help-output.
+    def banner(task, namespace = true, subcommand = true)
+      "#{basename} #{task.formatted_usage(self, true, subcommand).split(':').join(' ')}"
+    end
+
+  end
+end
diff --git a/lib/schleuder/conf.rb b/lib/schleuder/conf.rb
new file mode 100644
index 0000000..dc8c197
--- /dev/null
+++ b/lib/schleuder/conf.rb
@@ -0,0 +1,97 @@
+module Schleuder
+  class Conf
+    include Singleton
+
+    EMAIL_REGEXP = /\A.+ at .+\z/i
+
+    def config
+      @config ||= self.class.load_config('schleuder', ENV['SCHLEUDER_CONFIG'])
+    end
+
+    def self.load_config(defaults_basename, filename)
+      load_defaults(defaults_basename).deep_merge(load_config_file(filename))
+    end
+
+    def self.lists_dir
+      instance.config['lists_dir']
+    end
+
+    def self.plugins_dir
+      instance.config['plugins_dir']
+    end
+
+    def self.database
+      instance.config['database'][ENV['SCHLEUDER_ENV']]
+    end
+
+    def self.databases
+      instance.config['database']
+    end
+
+    def self.superadmin
+      instance.config['superadmin']
+    end
+
+    def self.log_level
+      instance.config['log_level'] || 'WARN'
+    end
+
+    def self.api
+      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
+
+    # Three legacy options
+    def self.smtp_host
+      instance.config['smtp_host']
+    end
+
+    def self.smtp_port
+      instance.config['smtp_port']
+    end
+
+    def self.smtp_helo_domain
+      instance.config['smtp_helo_domain']
+    end
+
+    def self.smtp_settings
+      settings = instance.config['smtp_settings'] || {}
+      # Support previously used config-options.
+      # Remove this in future versions.
+      {smtp_host: :address, smtp_port: :port, smtp_helo_domain: :domain}.each do |old, new|
+        value = self.send(old)
+        if value.present?
+          Schleuder.logger.warn "Deprecation warning: In schleuder.yml #{old} should be changed to smtp_settings[#{new}]."
+          settings[new] = value
+        end
+      end
+      settings
+    end
+
+    private
+
+    def self.load_config_file(filename)
+      file = Pathname.new(filename)
+      if file.readable?
+        YAML.load(file.read)
+      else
+        {}
+      end
+    end
+
+    def self.load_defaults(basename)
+      file = Pathname.new(ENV['SCHLEUDER_ROOT']).join("etc/#{basename}.yml")
+      if ! file.readable?
+        raise RuntimError, "Error: '#{file}' is not a readable file."
+      end
+      load_config_file(file)
+    end
+  end
+end
diff --git a/lib/schleuder/errors/active_model_error.rb b/lib/schleuder/errors/active_model_error.rb
new file mode 100644
index 0000000..f0deeae
--- /dev/null
+++ b/lib/schleuder/errors/active_model_error.rb
@@ -0,0 +1,15 @@
+module Schleuder
+  module Errors
+    class ActiveModelError < Base
+      def initialize(errors)
+        @errors = errors
+      end
+
+      def message
+        @errors.messages.map do |message|
+          message.join(' ')
+        end.join("\n")
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/base.rb b/lib/schleuder/errors/base.rb
new file mode 100644
index 0000000..da1a76a
--- /dev/null
+++ b/lib/schleuder/errors/base.rb
@@ -0,0 +1,17 @@
+module Schleuder
+  module Errors
+    class Base < StandardError
+      def t(*args)
+        I18n.t(*args)
+      end
+
+      def to_s
+        message + t('errors.signoff')
+      end
+
+      def set_default_locale
+        I18n.locale = I18n.default_locale
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/decryption_failed.rb b/lib/schleuder/errors/decryption_failed.rb
new file mode 100644
index 0000000..428dea4
--- /dev/null
+++ b/lib/schleuder/errors/decryption_failed.rb
@@ -0,0 +1,16 @@
+module Schleuder
+  module Errors
+    class DecryptionFailed < Base
+      def initialize(list)
+        set_default_locale
+        @list = list
+      end
+
+      def message
+        t('errors.decryption_failed',
+          { key: @list.key.to_s,
+            email: @list.sendkey_address })
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/file_not_found.rb b/lib/schleuder/errors/file_not_found.rb
new file mode 100644
index 0000000..5321c23
--- /dev/null
+++ b/lib/schleuder/errors/file_not_found.rb
@@ -0,0 +1,14 @@
+module Schleuder
+  module Errors
+    class FileNotFound < Base
+      def initialize(file)
+        @file = file
+      end
+
+      def message
+        t('errors.file_not_found', file: @file)
+      end
+    end
+  end
+end
+
diff --git a/lib/schleuder/errors/invalid_listname.rb b/lib/schleuder/errors/invalid_listname.rb
new file mode 100644
index 0000000..cd1e78f
--- /dev/null
+++ b/lib/schleuder/errors/invalid_listname.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class InvalidListname < Base
+      def initialize(listname)
+        @listname = listname
+      end
+
+      def message
+        t('errors.invalid_listname', email: @listname)
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/key_adduid_failed.rb b/lib/schleuder/errors/key_adduid_failed.rb
new file mode 100644
index 0000000..6af7694
--- /dev/null
+++ b/lib/schleuder/errors/key_adduid_failed.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class KeyAdduidFailed < Base
+      def initialize(errmsg)
+        @errmsg = errmsg
+      end
+
+      def message
+        t('errors.key_adduid_failed', { errmsg: @errmsg })
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/key_generation_failed.rb b/lib/schleuder/errors/key_generation_failed.rb
new file mode 100644
index 0000000..253b092
--- /dev/null
+++ b/lib/schleuder/errors/key_generation_failed.rb
@@ -0,0 +1,16 @@
+module Schleuder
+  module Errors
+    class KeyGenerationFailed < Base
+      def initialize(listdir, listname)
+        @listdir = listdir
+        @listname = listname
+      end
+
+      def message
+        t('errors.key_generation_failed',
+          { listdir: @listdir,
+            listname: @listname })
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/keyword_admin_only.rb b/lib/schleuder/errors/keyword_admin_only.rb
new file mode 100644
index 0000000..8d1f362
--- /dev/null
+++ b/lib/schleuder/errors/keyword_admin_only.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class KeywordAdminOnly
+      def initialize(keyword)
+        @keyword = keyword
+      end
+
+      def message
+        t('errors.keyword_admin_only', keyword: keyword)
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/list_exists.rb b/lib/schleuder/errors/list_exists.rb
new file mode 100644
index 0000000..4a3131c
--- /dev/null
+++ b/lib/schleuder/errors/list_exists.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class ListExists < Base
+      def initialize(listname)
+        @listname = listname
+      end
+
+      def message
+        t('errors.list_exists', email: @listname)
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/list_not_found.rb b/lib/schleuder/errors/list_not_found.rb
new file mode 100644
index 0000000..7302b31
--- /dev/null
+++ b/lib/schleuder/errors/list_not_found.rb
@@ -0,0 +1,14 @@
+module Schleuder
+  module Errors
+    class ListNotFound < Base
+      def initialize(recipient)
+        @recipient = recipient
+      end
+
+      def message
+        t('errors.list_not_found', email: @recipient)
+      end
+    end
+  end
+end
+
diff --git a/lib/schleuder/errors/list_property_missing.rb b/lib/schleuder/errors/list_property_missing.rb
new file mode 100644
index 0000000..b9c0111
--- /dev/null
+++ b/lib/schleuder/errors/list_property_missing.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class ListPropertyMissing < Base
+      def initialize(property)
+        @property = property
+      end
+
+      def to_s
+        t("errors.list_#{@property}_missing")
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/listdir_problem.rb b/lib/schleuder/errors/listdir_problem.rb
new file mode 100644
index 0000000..c68b45b
--- /dev/null
+++ b/lib/schleuder/errors/listdir_problem.rb
@@ -0,0 +1,16 @@
+module Schleuder
+  module Errors
+    class ListdirProblem < Base
+      def initialize(dir, problem)
+        @dir = dir
+        @problem = problem
+      end
+
+      def message
+        problem = t("errors.listdir_problem.#{@problem}")
+        t('errors.listdir_problem.message', dir: @dir, problem: problem)
+      end
+    end
+  end
+end
+
diff --git a/lib/schleuder/errors/loading_list_settings_failed.rb b/lib/schleuder/errors/loading_list_settings_failed.rb
new file mode 100644
index 0000000..899d8ab
--- /dev/null
+++ b/lib/schleuder/errors/loading_list_settings_failed.rb
@@ -0,0 +1,14 @@
+module Schleuder
+  module Errors
+    class LoadingListSettingsFailed < Base
+      def initialize
+        @config_file = ENV['SCHLEUDER_LIST_DEFAULTS']
+      end
+
+      def message
+        t('errors.loading_list_settings_failed', config_file: @config_file)
+      end
+    end
+  end
+end
+
diff --git a/lib/schleuder/errors/message_empty.rb b/lib/schleuder/errors/message_empty.rb
new file mode 100644
index 0000000..66205f7
--- /dev/null
+++ b/lib/schleuder/errors/message_empty.rb
@@ -0,0 +1,14 @@
+module Schleuder
+  module Errors
+    class MessageEmpty < Base
+      def initialize(list)
+        set_default_locale
+        @request_address = list.request_address
+      end
+
+      def message
+        t('errors.message_empty', { request_address: @request_address })
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/message_not_from_admin.rb b/lib/schleuder/errors/message_not_from_admin.rb
new file mode 100644
index 0000000..5e1029b
--- /dev/null
+++ b/lib/schleuder/errors/message_not_from_admin.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class MessageNotFromAdmin < Base
+      def initialize(list)
+        set_default_locale
+      end
+
+      def message
+        t('errors.message_not_from_admin')
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/message_sender_not_subscribed.rb b/lib/schleuder/errors/message_sender_not_subscribed.rb
new file mode 100644
index 0000000..abd586c
--- /dev/null
+++ b/lib/schleuder/errors/message_sender_not_subscribed.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class MessageSenderNotSubscribed < Base
+      def initialize(list)
+        set_default_locale
+      end
+
+      def message
+        t('errors.message_sender_not_subscribed')
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/message_too_big.rb b/lib/schleuder/errors/message_too_big.rb
new file mode 100644
index 0000000..5efcebf
--- /dev/null
+++ b/lib/schleuder/errors/message_too_big.rb
@@ -0,0 +1,14 @@
+module Schleuder
+  module Errors
+    class MessageTooBig < Base
+      def initialize(list)
+        set_default_locale
+        @allowed_size = list.max_message_size_kb
+      end
+
+      def message
+        t('errors.message_too_big', { allowed_size: @allowed_size })
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/message_unauthenticated.rb b/lib/schleuder/errors/message_unauthenticated.rb
new file mode 100644
index 0000000..06be5b8
--- /dev/null
+++ b/lib/schleuder/errors/message_unauthenticated.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class MessageUnauthenticated < Base
+      def initialize
+        set_default_locale
+      end
+
+      def message
+        t('errors.message_unauthenticated')
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/message_unencrypted.rb b/lib/schleuder/errors/message_unencrypted.rb
new file mode 100644
index 0000000..b69984d
--- /dev/null
+++ b/lib/schleuder/errors/message_unencrypted.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class MessageUnencrypted < Base
+      def initialize(list)
+        set_default_locale
+      end
+
+      def message
+        t('errors.message_unencrypted')
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/message_unsigned.rb b/lib/schleuder/errors/message_unsigned.rb
new file mode 100644
index 0000000..fe18aee
--- /dev/null
+++ b/lib/schleuder/errors/message_unsigned.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Errors
+    class MessageUnsigned < Base
+      def initialize(list)
+        set_default_locale
+      end
+
+      def message
+        t('errors.message_unsigned')
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/errors/standard_error.rb b/lib/schleuder/errors/standard_error.rb
new file mode 100644
index 0000000..921af4a
--- /dev/null
+++ b/lib/schleuder/errors/standard_error.rb
@@ -0,0 +1,5 @@
+class StandardError
+  def message_with_backtrace
+    "#{message}\n#{self.backtrace.join("\n")}\n"
+  end
+end
diff --git a/lib/schleuder/errors/too_many_keys.rb b/lib/schleuder/errors/too_many_keys.rb
new file mode 100644
index 0000000..1b5be7c
--- /dev/null
+++ b/lib/schleuder/errors/too_many_keys.rb
@@ -0,0 +1,17 @@
+module Schleuder
+  module Errors
+    class TooManyKeys < Base
+      def initialize(listdir, listname)
+        @listdir = listdir
+        @listname = listname
+      end
+
+      def message
+        t('errors.too_many_keys',
+          { listdir: @listdir,
+            listname: @listname })
+      end
+    end
+  end
+end
+
diff --git a/lib/schleuder/errors/unknown_list_option.rb b/lib/schleuder/errors/unknown_list_option.rb
new file mode 100644
index 0000000..7351888
--- /dev/null
+++ b/lib/schleuder/errors/unknown_list_option.rb
@@ -0,0 +1,14 @@
+module Schleuder
+  module Errors
+    class UnknownListOption < Base
+      def initialize(exception)
+        @option = exception.attribute
+        @config_file = ENV['SCHLEUDER_LIST_DEFAULTS']
+      end
+
+      def message
+        t('errors.unknown_list_option', option: @option, config_file: @config_file)
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/filters/auth_filter.rb b/lib/schleuder/filters/auth_filter.rb
new file mode 100644
index 0000000..7e402c2
--- /dev/null
+++ b/lib/schleuder/filters/auth_filter.rb
@@ -0,0 +1,39 @@
+module Schleuder
+  module Filters
+
+    def self.receive_encrypted_only(list, mail)
+      if list.receive_encrypted_only? && ! mail.was_encrypted?
+        list.logger.info "Rejecting mail as unencrypted"
+        return Errors::MessageUnencrypted.new(list)
+      end
+    end
+
+    def self.receive_signed_only(list, mail)
+      if list.receive_signed_only? && ! mail.was_validly_signed?
+        list.logger.info "Rejecting mail as unsigned"
+        return Errors::MessageUnsigned.new(list)
+      end
+    end
+
+    def self.receive_authenticated_only(list, mail)
+      if list.receive_authenticated_only? && ( ! mail.was_encrypted? || ! mail.was_validly_signed? )
+        list.logger.info "Rejecting mail as unauthenticated"
+        return Errors::MessageUnauthenticated.new
+      end
+    end
+
+    def self.receive_from_subscribed_emailaddresses_only(list, mail)
+      if list.receive_from_subscribed_emailaddresses_only? && list.subscriptions.where(email: mail.from.first).blank?
+        list.logger.info "Rejecting mail as not from subscribed address."
+        return Errors::MessageSenderNotSubscribed.new(list)
+      end
+    end
+
+    def self.receive_admin_only(list, mail)
+      if list.receive_admin_only? && ( ! mail.was_validly_signed? || ! mail.signer.admin? )
+        list.logger.info "Rejecting mail as not from admin."
+        return Errors::MessageNotFromAdmin.new(list)
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/filters/bounces_filter.rb b/lib/schleuder/filters/bounces_filter.rb
new file mode 100644
index 0000000..dfac397
--- /dev/null
+++ b/lib/schleuder/filters/bounces_filter.rb
@@ -0,0 +1,12 @@
+module Schleuder
+  module Filters
+    def self.forward_bounce_to_admins(list, mail)
+      if mail.bounce?
+        list.logger.info "Forwarding bounce to admins"
+        list.logger.notify_admin I18n.t(:forward_bounce_to_admins), mail.original_message, I18n.t('bounce')
+        exit
+      end
+    end
+  end
+end
+
diff --git a/lib/schleuder/filters/forward_filter.rb b/lib/schleuder/filters/forward_filter.rb
new file mode 100644
index 0000000..caf0be5
--- /dev/null
+++ b/lib/schleuder/filters/forward_filter.rb
@@ -0,0 +1,17 @@
+module Schleuder
+  module Filters
+    def self.forward_to_owner(list, mail)
+      return if ! mail.to_owner?
+
+      list.logger.debug "Forwarding addressed to -owner"
+      mail.add_pseudoheader(:note, I18n.t(:owner_forward_prefix))
+      cleanmail = mail.clean_copy(true)
+      list.admins.each do |admin|
+        list.logger.debug "Forwarding message to #{admin}"
+        admin.send_mail(cleanmail)
+      end
+      exit
+    end
+  end
+end
+
diff --git a/lib/schleuder/filters/forward_incoming.rb b/lib/schleuder/filters/forward_incoming.rb
new file mode 100644
index 0000000..13184b2
--- /dev/null
+++ b/lib/schleuder/filters/forward_incoming.rb
@@ -0,0 +1,13 @@
+module Schleuder
+  module Filters
+
+    def self.forward_all_incoming_to_admins(list, mail)
+      if list.forward_all_incoming_to_admins
+        list.logger.notify_admin I18n.t(:forward_all_incoming_to_admins), mail.original_message, I18n.t('incoming_message')
+      end
+    end
+
+  end
+end
+
+
diff --git a/lib/schleuder/filters/max_message_size.rb b/lib/schleuder/filters/max_message_size.rb
new file mode 100644
index 0000000..83014dc
--- /dev/null
+++ b/lib/schleuder/filters/max_message_size.rb
@@ -0,0 +1,14 @@
+module Schleuder
+  module Filters
+
+    def self.max_message_size(list, mail)
+      if (mail.raw_source.size / 1024) > list.max_message_size_kb
+        list.logger.info "Rejecting mail as too big"
+        return Errors::MessageTooBig.new(list)
+      end
+    end
+
+  end
+end
+
+
diff --git a/lib/schleuder/filters/request_filter.rb b/lib/schleuder/filters/request_filter.rb
new file mode 100644
index 0000000..024d193
--- /dev/null
+++ b/lib/schleuder/filters/request_filter.rb
@@ -0,0 +1,22 @@
+module Schleuder
+  module Filters
+    def self.request(list, mail)
+      return if ! mail.request?
+
+      list.logger.debug "Request-message"
+
+      if ! mail.was_encrypted? || ! mail.was_validly_signed?
+        list.logger.debug "Error: Message was not encrypted and validly signed"
+        return Errors::MessageUnauthenticated.new
+      end
+
+      if mail.keywords.empty?
+        output = I18n.t(:no_keywords_error)
+      else
+        output = Plugins::Runner.run(list, mail).compact
+      end
+      mail.reply_to_signer(output)
+      exit
+    end
+  end
+end
diff --git a/lib/schleuder/filters/send_key_filter.rb b/lib/schleuder/filters/send_key_filter.rb
new file mode 100644
index 0000000..3fbc845
--- /dev/null
+++ b/lib/schleuder/filters/send_key_filter.rb
@@ -0,0 +1,27 @@
+module Schleuder
+  module Filters
+    def self.send_key(list, mail)
+      return if ! mail.sendkey_request?
+
+      list.logger.debug "Sending public key as reply."
+
+      out = mail.reply
+      out.from = list.email
+      # We're not sending to a subscribed address, so we need to specify a return-path manually.
+      out.return_path = list.bounce_address
+      out.body = I18n.t(:list_public_key_attached)
+      # TODO: clean this up, there must be an easier way to attach inline-disposited content.
+      filename = "#{list.email}.asc"
+      out.add_file({
+        filename: filename,
+        content: list.export_key
+      })
+      out.attachments[filename].content_type = 'application/pgp-keys'
+      out.attachments[filename].content_description = 'OpenPGP public key'
+      # TODO: find out why the gpg-module puts all the headers into the first mime-part, too
+      out.gpg sign: true
+      out.deliver
+      exit
+    end
+  end
+end
diff --git a/lib/schleuder/filters_runner.rb b/lib/schleuder/filters_runner.rb
new file mode 100644
index 0000000..4dc4ced
--- /dev/null
+++ b/lib/schleuder/filters_runner.rb
@@ -0,0 +1,83 @@
+module Schleuder
+  module Filters
+    class Runner
+      # To define priority sort this.
+      FILTERS = %w[
+        request
+        forward_bounce_to_admins
+        forward_all_incoming_to_admins
+        send_key
+        forward_to_owner
+        max_message_size
+        receive_admin_only
+        receive_authenticated_only
+        receive_signed_only
+        receive_encrypted_only
+        receive_from_subscribed_emailaddresses_only
+      ]
+
+      def self.run(list, mail)
+        @list = list
+        @mail = mail
+        FILTERS.map do |cmd|
+          @list.logger.debug "Calling filter #{cmd}"
+          response = Filters.send(cmd, list, mail)
+          if stop?(response)
+            if bounce?(response)
+              return response
+            else
+              return nil
+            end
+          end
+        end
+        nil
+      end
+
+      def self.stop?(response)
+        response.kind_of?(StandardError)
+      end
+
+      def self.bounce?(response)
+        if @list.bounces_drop_all
+          @list.logger.debug "Dropping bounce as configurated"
+          notify_admins(I18n.t('.bounces_drop_all'))
+          return false
+        end
+
+        @list.bounces_drop_on_headers.each do |key, value|
+          if @mail.headers[key].to_s.match(/${value}/i)
+            @list.logger.debug "Incoming message header key '#{key}' matches value '#{value}': dropping the bounce."
+            notify_admins(I18n.t('.bounces_drop_on_headers', key: key, value: value))
+            return false
+          end
+        end
+
+        @list.logger.debug "Bouncing message"
+        true
+      end
+
+      def self.notify_admins(reason)
+        if @list.bounces_notify_admins?
+          @list.logger.notify_admin reason, @mail.original_message, I18n.t('notice')
+        end
+      end
+
+      def self.reply_to_sender(msg)
+        sender_addr = @mail.from.first
+        logger.debug "Replying to #{sender_addr.inspect}"
+        reply = @mail.reply
+        reply.from = @list.email
+        reply.return_path = @list.bounce_address
+        reply.body = msg
+        gpg_opts = {sign: true}
+        if @list.keys("<#{sender_addr}>").present?
+          logger.debug "Found key for address"
+          gpg_opts[encrypt] = true
+        end
+        reply.gpg gpg_opts
+        list.logger.info "Sending message to #{sender_addr}"
+        reply.deliver
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/gpgme/ctx.rb b/lib/schleuder/gpgme/ctx.rb
new file mode 100644
index 0000000..234c90a
--- /dev/null
+++ b/lib/schleuder/gpgme/ctx.rb
@@ -0,0 +1,39 @@
+module GPGME
+  class Ctx
+    def keyimport(*args)
+      self.import_keys(*args)
+      result = self.import_result
+      result.imports.map(&:set_action)
+      result
+    end
+
+    # Tell gpgme to use the given binary.
+    def self.set_gpg_path_from_env
+      path = ENV['GPGBIN'].to_s
+      if ! path.empty?
+        puts "setting gpg to use #{path}"
+        GPGME::Engine.set_info(GPGME::PROTOCOL_OpenPGP, path, ENV['GNUPGHOME'])
+        if gpg_engine.version.nil?
+          $stderr.puts "Error: The binary you specified doesn't provide a gpg-version."
+          exit 1
+        end
+      end
+    end
+
+    def self.sufficient_gpg_version?(required)
+      Gem::Version.new(required) < Gem::Version.new(gpg_engine.version)
+    end
+
+    def self.check_gpg_version
+      set_gpg_path_from_env
+      if ! sufficient_gpg_version?('2.0')
+        $stderr.puts "Error: GnuPG version >= 2.0 required.\nPlease install it and/or provide the path to the binary via the environment-variable GPGBIN.\nExample: GPGBIN=/opt/gpg2/bin/gpg ..."
+        exit 1
+      end
+    end
+
+    def self.gpg_engine
+      GPGME::Engine.info.find {|e| e.protocol == GPGME::PROTOCOL_OpenPGP }
+    end
+  end
+end
diff --git a/lib/schleuder/gpgme/import_status.rb b/lib/schleuder/gpgme/import_status.rb
new file mode 100644
index 0000000..f82d151
--- /dev/null
+++ b/lib/schleuder/gpgme/import_status.rb
@@ -0,0 +1,27 @@
+module GPGME
+  class ImportStatus
+    attr_reader :action
+
+    # Unfortunately in initialize() @status and @result are not yet intialized.
+    def set_action
+      @action ||= if self.status > 0
+                    'imported'
+                  elsif self.result == 0
+                    'unchanged'
+                  else
+                    # An error happened.
+                    # TODO: Give details by going through the list of errors in
+                    # "gpg-errors.h" and find out which is present here.
+                    'not imported'
+                  end
+      self
+    end
+
+    # Force encoding, some databases save "ASCII-8BIT" as binary data.
+    alias_method :orig_fingerprint, :fingerprint
+    def fingerprint
+      orig_fingerprint.encode(Encoding::US_ASCII)
+    end
+
+  end
+end
diff --git a/lib/schleuder/gpgme/key.rb b/lib/schleuder/gpgme/key.rb
new file mode 100644
index 0000000..4bb06d9
--- /dev/null
+++ b/lib/schleuder/gpgme/key.rb
@@ -0,0 +1,44 @@
+module GPGME
+  class Key
+    # Overwrite to specify the full fingerprint instead of the short key-ID.
+    def to_s
+      primary_subkey = subkeys[0]
+      s = sprintf("%s   %4d%s/%s %s\n",
+                  primary_subkey.secret? ? 'sec' : 'pub',
+                  primary_subkey.length,
+                  primary_subkey.pubkey_algo_letter,
+                  primary_subkey.fingerprint,
+                  primary_subkey.timestamp.strftime('%Y-%m-%d'))
+      uids.each do |user_id|
+        s << "uid\t\t#{user_id.name} <#{user_id.email}>\n"
+      end
+      subkeys.each do |subkey|
+        s << subkey.to_s
+      end
+      s
+    end
+
+    def armored
+      "#{self.to_s}\n\n#{export(armor: true).read}"
+    end
+
+    # Force encoding, some databases save "ASCII-8BIT" as binary data.
+    alias_method :orig_fingerprint, :fingerprint
+    def fingerprint
+      orig_fingerprint.encode(Encoding::US_ASCII)
+    end
+
+    def adduid(uid, newuid, homedir)
+      output = ''
+      exitcode = -1
+      # Specifying the key via fingerprint apparently doesn't work.
+      cmd = "gpg --homedir '#{homedir}' --quick-adduid #{uid} '#{uid} <#{newuid}>'"
+      Open3.popen2e(cmd) do |stdin, stdout_err, wait_thr|
+        output = stdout_err.readlines.join
+        exitcode = wait_thr.value
+      end
+
+      [exitcode.to_i, output.to_s]
+    end
+  end
+end
diff --git a/lib/schleuder/gpgme/sub_key.rb b/lib/schleuder/gpgme/sub_key.rb
new file mode 100644
index 0000000..32baa6a
--- /dev/null
+++ b/lib/schleuder/gpgme/sub_key.rb
@@ -0,0 +1,13 @@
+module GPGME
+  class SubKey
+    # Overwrite to specify the full fingerprint instead of the short key-ID.
+    def to_s
+      sprintf("%s   %4d%s/%s %s\n",
+              secret? ? 'ssc' : 'sub',
+              length,
+              pubkey_algo_letter,
+              fingerprint,
+              timestamp.strftime('%Y-%m-%d'))
+    end
+  end
+end
diff --git a/lib/schleuder/list.rb b/lib/schleuder/list.rb
new file mode 100644
index 0000000..814b836
--- /dev/null
+++ b/lib/schleuder/list.rb
@@ -0,0 +1,295 @@
+module Schleuder
+  class List < ActiveRecord::Base
+
+    has_many :subscriptions, dependent: :destroy
+    before_destroy :delete_listdir
+
+    serialize :headers_to_meta, JSON
+    serialize :bounces_drop_on_headers, JSON
+    serialize :keywords_admin_only, JSON
+    serialize :keywords_admin_notify, JSON
+
+    validates :email, presence: true, uniqueness: true, email: true
+    validates :fingerprint, presence: true, fingerprint: true
+    validates :send_encrypted_only,
+        :receive_encrypted_only,
+        :receive_signed_only,
+        :receive_authenticated_only,
+        :receive_from_subscribed_emailaddresses_only,
+        :receive_admin_only,
+        :keep_msgid,
+        :bounces_drop_all,
+        :bounces_notify_admins,
+        :include_list_headers,
+        :include_openpgp_header,
+        :forward_all_incoming_to_admins, boolean: true
+    validates_each :headers_to_meta,
+        :keywords_admin_only,
+        :keywords_admin_notify do |record, attrib, value|
+          value.each do |word|
+            if word !~ /\A[a-z_-]+\z/i
+              record.errors.add(attrib, I18n.t("errors.invalid_characters"))
+            end
+          end
+        end
+    validates_each :bounces_drop_on_headers do |record, attrib, value|
+          value.each do |key, val|
+            if key.to_s !~ /\A[a-z-]+\z/i || val.to_s !~ /\A[[:graph:]]+\z/i
+              record.errors.add(attrib, I18n.t("errors.invalid_characters"))
+            end
+          end
+        end
+    validates :subject_prefix,
+        :subject_prefix_in,
+        :subject_prefix_out,
+        no_line_breaks: true
+    validates :openpgp_header_preference,
+                presence: true,
+                inclusion: {
+                  in: %w(sign encrypt signencrypt unprotected none),
+                }
+    validates :max_message_size_kb, :logfiles_to_keep, greater_than_zero: true
+    validates :log_level,
+              presence: true,
+              inclusion: {
+                in: %w(debug info warn error),
+              }
+    validates :language,
+              presence: true,
+              inclusion: {
+                # TODO: find out why we break translations and available_locales if we use I18n.available_locales here.
+                in: %w(de en),
+              }
+    validates :public_footer,
+              allow_blank: true,
+              format: {
+                with: /\A[[:graph:]\s]*\z/i,
+              }
+
+    def self.configurable_attributes
+      @configurable_attributes ||= begin
+        all = self.validators.map(&:attributes).flatten.uniq.compact.sort
+        all - [:email, :fingerprint]
+      end
+    end
+
+    def logfile
+      @logfile ||= File.join(self.listdir, 'list.log')
+    end
+
+    def logger
+      @logger ||= Listlogger.new(self)
+    end
+
+    def to_s
+      email
+    end
+
+    def admins
+      subscriptions.where(admin: true)
+    end
+
+    def key(fingerprint=self.fingerprint)
+      keys(fingerprint).first
+    end
+
+    def secret_key
+      gpg.keys(self.fingerprint, true).first
+    end
+
+    def keys(identifier='')
+      gpg.keys(identifier)
+    end
+
+    def keys_by_email(address)
+      keys("<#{address}>")
+    end
+
+    def import_key(importable)
+      gpg.keyimport(GPGME::Data.new(importable))
+    end
+
+    def delete_key(fingerprint)
+      if key = gpg.keys(fingerprint).first
+        key.delete!
+        true
+      else
+        false
+      end
+    end
+
+    def export_key(fingerprint=self.fingerprint)
+      key = keys(fingerprint).first
+      if key.blank?
+        return false
+      end
+      key.armored
+    end
+
+    def check_keys
+      now = Time.now
+      checkdate = now + (60 * 60 * 24 * 14) # two weeks
+      unusable = []
+      expiring = []
+
+      keys.each do |key|
+        expiry = key.subkeys.first.expires
+        if expiry && expiry > now && expiry < checkdate
+          # key expires in the near future
+          expdays = ((exp - now)/86400).to_i
+          expiring << [key, expdays]
+        end
+
+        if key.trust
+          unusable << [key, key.trust]
+        end
+      end
+
+      text = ''
+      expiring.each do |key,days|
+        text << I18n.t('key_expires', {
+                          days: days,
+                          fingerprint: key.fingerprint,
+                          email: key.email
+                      })
+      end
+
+      unusable.each do |key,trust|
+        text << I18n.t('key_unusable', {
+                          trust: Array(trust).join(', '),
+                          fingerprint: key.fingerprint,
+                          email: key.email
+                      })
+      end
+      text
+    end
+
+    def self.by_recipient(recipient)
+      listname = recipient.gsub(/-(sendkey|request|owner|bounce)@/, '@')
+      where(email: listname).first
+    end
+
+    def sendkey_address
+      @sendkey_address ||= email.gsub('@', '-sendkey@')
+    end
+
+    def request_address
+      @request_address ||= email.gsub('@', '-request@')
+    end
+
+    def owner_address
+      @owner_address ||= email.gsub('@', '-owner@')
+    end
+
+    def bounce_address
+      @bounce_address ||= email.gsub('@', '-bounce@')
+    end
+
+    def gpg
+      @gpg_ctx ||= begin
+        # TODO: figure out why set it again...
+        # Set GNUPGHOME when list is created.
+        ENV['GNUPGHOME'] = listdir
+        GPGME::Ctx.new armor: true
+      end
+    end
+
+    # TODO: place this somewhere sensible.
+    # Call cleanup when script finishes.
+    #Signal.trap(0, proc { @list.cleanup })
+    def cleanup
+      if @gpg_agent_pid
+        Process.kill('TERM', @gpg_agent_pid.to_i)
+      end
+    rescue => e
+      $stderr.puts "Failed to kill gpg-agent: #{e}"
+    end
+
+    def fingerprint=(arg)
+      # Strip whitespace from incoming arg.
+      if arg
+        write_attribute(:fingerprint, arg.gsub(/\s*/, '').chomp)
+      end
+    end
+
+    def self.listdir(listname)
+      File.join(
+          Conf.lists_dir,
+          listname.split('@').reverse
+        )
+    end
+
+    def listdir
+      @listdir ||= self.class.listdir(self.email)
+    end
+
+    def subscribe(email, fingerprint=nil, adminflag=false, deliveryflag=true)
+      adminflag ||= false
+      deliveryflag ||= true
+      sub = Subscription.new(
+          list_id: self.id,
+          email: email,
+          fingerprint: fingerprint,
+          admin: adminflag,
+          delivery_enabled: deliveryflag
+        )
+      sub.save
+      sub
+    end
+
+    def unsubscribe(email, delete_key=false)
+      sub = subscriptions.where(email: email).first
+      if sub.blank?
+        false
+      end
+
+      if ! sub.destroy
+        return sub
+      end
+
+      if delete_key
+        sub.delete_key
+      end
+    end
+
+    def keywords_admin_notify
+      Array(read_attribute(:keywords_admin_notify))
+    end
+
+    def keywords_admin_only
+      Array(read_attribute(:keywords_admin_only))
+    end
+
+    def admin_only?(keyword)
+      keywords_admin_only.include?(keyword)
+    end
+
+    def from_admin?(mail)
+      return false if ! mail.was_validly_signed?
+      admins.find do |admin|
+        admin.fingerprint == mail.signature.fingerprint
+      end.presence || false
+    end
+
+    def set_attribute(attrib, value)
+      self.send("#{attrib}=", value)
+    end
+
+    private
+
+      def delete_listdir
+        if File.exists?(self.listdir)
+          FileUtils.rm_r(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, directly not present"
+        end
+        true
+      rescue => exc
+        # Don't use list-logger here — if the list-dir isn't present we can't log to it!
+        Schleuder.logger.error "Error while deleting listdir: #{exc}"
+        return false
+      end
+  end
+end
diff --git a/lib/schleuder/list_builder.rb b/lib/schleuder/list_builder.rb
new file mode 100644
index 0000000..f1b5591
--- /dev/null
+++ b/lib/schleuder/list_builder.rb
@@ -0,0 +1,152 @@
+module Schleuder
+  class ListBuilder
+    def initialize(list_attributes, adminemail=nil, adminkey=nil)
+      @list_attributes = list_attributes.with_indifferent_access
+      @listname = list_attributes[:email]
+      @fingerprint = list_attributes[:fingerprint]
+      @adminemail = adminemail
+      @adminkey = adminkey
+      @messages = []
+    end
+
+    def read_default_settings
+      hash = Conf.load_config('list-defaults', ENV['SCHLEUDER_LIST_DEFAULTS'])
+      if ! hash.kind_of?(Hash)
+        raise Errors::LoadingListSettingsFailed.new
+      end
+      hash
+    rescue Psych::SyntaxError
+      raise Errors::LoadingListSettingsFailed.new
+    end
+
+    def run
+      Schleuder.logger.info "Building new list"
+
+      if @listname.blank? || ! @listname.match(Conf::EMAIL_REGEXP)
+        return [nil, "Given 'listname' is not a valid email address."]
+      end
+
+      settings = read_default_settings.merge(@list_attributes)
+      list = List.new(settings)
+
+      @list_dir = list.listdir
+      create_or_test_list_dir
+
+      if list.fingerprint.blank?
+        list_key = gpg.keys("<#{list.email}>").first
+        if list_key.nil?
+          list_key = create_key(list)
+        end
+        list.fingerprint = list_key.fingerprint
+      end
+
+      if ! list.valid?
+        return list
+      end
+
+      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?
+          admin_key = list.keys_by_email(@adminemail).first
+          if admin_key.present?
+            admin_fpr = admin_key.fingerprint
+          end
+        end
+        sub = list.subscribe(@adminemail, admin_fpr, true)
+        if sub.errors.present?
+          raise ActiveModelError.new(sub.errors)
+        end
+      end
+
+      [list, @messages]
+    end
+
+    def gpg
+      @gpg_ctx ||= begin
+        ENV["GNUPGHOME"] = @list_dir
+        GPGME::Ctx.new
+      end
+    end
+
+    def create_key(list)
+      Schleuder.logger.info "Generating key-pair, this could take a while..."
+      gpg.generate_key(key_params(list))
+
+      # Get key without knowing the fingerprint yet.
+      keys = list.keys_by_email(@listname)
+      if keys.empty?
+        raise Errors::KeyGenerationFailed.new(@list_dir, @listname)
+      elsif keys.size > 1
+        raise Errors::TooManyKeys.new(@list_dir, @listname)
+      else
+        adduids(list, keys.first)
+      end
+
+      keys.first
+    end
+
+    def adduids(list, key)
+      # Add UIDs for -owner and -request.
+      gpg_version = `gpg --version`.lines.first.split.last
+      # Gem::Version knows that e.g. ".10" is higher than ".4", String doesn't.
+      if Gem::Version.new(gpg_version) < Gem::Version.new("2.1.4")
+        string = "Couldn't add additional UIDs to the list's key automatically (GnuPG version >= 2.1.4 is required for that, using 'gpg' in PATH).\nPlease add these UIDs to the list's key manually: #{list.request_address}, #{list.owner_address}."
+        # Don't add to errors because then the list isn't saved.
+        @messages << Errors::KeyAdduidFailed.new(string).message
+        return false
+      end
+
+      [list.request_address, list.owner_address].each do |address|
+        err, string = key.adduid(list.email, address, list.listdir)
+        if err > 0
+          raise Errors::KeyAdduidFailed.new(string)
+        end
+      end
+    rescue Errno::ENOENT
+      raise Errors::KeyAdduidFailed.new('Need gpg in $PATH')
+    end
+
+    def key_params(list)
+      "
+        <GnupgKeyParms format=\"internal\">
+        Key-Type: RSA
+        Key-Length: 4096
+        Subkey-Type: RSA
+        Subkey-Length: 4096
+        Name-Real: #{list.email}
+        Name-Email: #{list.email}
+        Expire-Date: 0
+        %no-protection
+        </GnupgKeyParms>
+
+      "
+    end
+
+    def create_or_test_list_dir
+      if File.exists?(@list_dir)
+        if ! File.directory?(@list_dir)
+          raise Errors::ListdirProblem.new(@list_dir, :not_a_directory)
+        end
+
+        if ! File.writable?(@list_dir)
+          raise Errors::ListdirProblem.new(@list_dir, :not_writable)
+        end
+      else
+        FileUtils.mkdir_p(@list_dir, mode: 0700)
+      end
+    end
+
+  end
+end
diff --git a/lib/schleuder/listlogger.rb b/lib/schleuder/listlogger.rb
new file mode 100644
index 0000000..67939cd
--- /dev/null
+++ b/lib/schleuder/listlogger.rb
@@ -0,0 +1,30 @@
+module Schleuder
+  class Listlogger < ::Logger
+    include LoggerNotifications
+    def initialize(list)
+      super(list.logfile, 'daily')
+      @from = list.email
+      @adminaddresses = list.admins.map(&:email)
+      @level = ::Logger.const_get(list.log_level.upcase)
+      remove_old_logfiles(list)
+    end
+
+    # Logger rotates but doesn't delete older files, so we're helping
+    # ourselves.
+    def remove_old_logfiles(list)
+      logfiles_to_keep = list.logfiles_to_keep.to_i
+      if logfiles_to_keep < 1
+        logfiles_to_keep = list.class.column_defaults['logfiles_to_keep']
+      end
+      suffix_now = Time.now.strftime("%Y%m%d").to_i
+      del_older_than = suffix_now - logfiles_to_keep
+      Pathname.glob("#{list.logfile}.????????").each do |file|
+        if file.basename.to_s.match(/\.([0-9]{8})$/)
+          if del_older_than.to_i >= $1.to_i
+            file.unlink
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/logger.rb b/lib/schleuder/logger.rb
new file mode 100644
index 0000000..736cc80
--- /dev/null
+++ b/lib/schleuder/logger.rb
@@ -0,0 +1,23 @@
+module Schleuder
+  def logger
+    @logger ||= Logger.new
+  end
+  module_function :logger
+
+  class Logger < Syslog::Logger
+    include LoggerNotifications
+    def initialize
+      if RUBY_VERSION.to_f < 2.1
+        super('Schleuder')
+      else
+        super('Schleuder', Syslog::LOG_MAIL)
+      end
+      # We need some sender-address different from the superadmin-address.
+      @from = "#{`whoami`.chomp}@#{`hostname`.chomp}"
+      @adminaddresses = Conf.superadmin
+      @level = ::Logger.const_get(Conf.log_level.upcase)
+    end
+  end
+
+end
+
diff --git a/lib/schleuder/logger_notifications.rb b/lib/schleuder/logger_notifications.rb
new file mode 100644
index 0000000..c503017
--- /dev/null
+++ b/lib/schleuder/logger_notifications.rb
@@ -0,0 +1,50 @@
+module Schleuder
+  module LoggerNotifications
+    def adminaddresses
+      @adminaddresses.presence || Conf.superadmin.presence || 'root at localhost'
+    end
+
+    def error(string)
+      super(string)
+      notify_admin(string)
+    end
+
+    def fatal(string, original_message=nil)
+      super(string.to_s + append_original_message(original_message))
+      notify_admin(string, original_message)
+    end
+
+    def notify_admin(string, original_message=nil, subject='Error')
+      Array(adminaddresses).each do |address|
+        mail = Mail.new
+        mail.from = @from
+        mail.to = address
+        mail.subject = subject
+        msgpart = Mail::Part.new
+        msgpart.charset = 'UTF-8'
+        msgpart.body = string.to_s
+        mail.add_part msgpart
+        if original_message
+          orig_part = Mail::Part.new
+          orig_part.content_type = 'message/rfc822'
+          orig_part.content_description = 'The originally incoming message'
+          orig_part.body = original_message.to_s
+          mail.add_part orig_part
+        end
+        mail.deliver
+      end
+      true
+    end
+
+    private
+
+    def append_original_message(original_message)
+      if original_message
+        "\n\nOriginal message:\n\n#{original_message.to_s}"
+      else
+        ''
+      end
+    end
+  end
+end
+
diff --git a/lib/schleuder/mail/message.rb b/lib/schleuder/mail/message.rb
new file mode 100644
index 0000000..923e9aa
--- /dev/null
+++ b/lib/schleuder/mail/message.rb
@@ -0,0 +1,302 @@
+module Mail
+  # TODO: Test if subclassing breaks integration of mail-gpg.
+  class Message
+    attr_accessor :recipient
+    attr_accessor :original_message
+    attr_accessor :list
+
+    # TODO: This should be in initialize(), but I couldn't understand the
+    # strange errors about wrong number of arguments when overriding
+    # Message#initialize.
+    def setup(recipient, list)
+      if self.encrypted?
+        new = self.decrypt(verify: true)
+      elsif self.signed?
+        new = self.verify
+      else
+        new = self
+      end
+
+      new.list = list
+      new.original_message = self.dup.freeze
+      new.recipient = recipient
+      new
+    end
+
+    def clean_copy(with_pseudoheaders=false)
+      clean = Mail.new
+      clean.from = list.email
+      clean.subject = self.subject
+
+      clean.add_msgids(list, self)
+      clean.add_list_headers(list)
+      clean.add_openpgp_headers(list)
+
+      if with_pseudoheaders
+        new_part = Mail::Part.new
+        new_part.body = self.pseudoheaders(list)
+        clean.add_part new_part
+      end
+
+      # Attach body or mime-parts, respectively.
+      if self.multipart?
+        self.parts.each do |part|
+          clean.add_part Mail::Part.new(part)
+        end
+      else
+        clean.add_part Mail::Part.new(self.body)
+      end
+      clean
+    end
+
+    def prepend_part(part)
+      self.add_part(part)
+      self.parts.unshift(parts.delete_at(parts.size-1))
+    end
+
+    def was_encrypted?
+      Mail::Gpg.encrypted?(original_message)
+    end
+
+    def was_encrypted_mime?
+      Mail::Gpg.encrypted_mime?(original_message)
+    end
+
+    def signature
+      # Theoretically there might be more than one signing key, in practice this is neglectable.
+      signatures.try(:first)
+    end
+
+    def was_validly_signed?
+      signature.present? && signature.valid? && signer.present?
+    end
+
+    def signer
+      if fingerprint = self.signature.try(:fpr)
+        list.subscriptions.where(fingerprint: fingerprint).first
+      end
+    end
+
+    def reply_to_signer(output)
+      reply = self.reply
+      reply.body = Array(output).join("\n")
+      self.signer.send_mail(reply)
+    end
+
+    def sendkey_request?
+      @recipient.match(/-sendkey@/)
+    end
+
+    def to_owner?
+      @recipient.match(/-owner@/)
+    end
+
+    def request?
+      @recipient.match(/-request@/)
+    end
+
+    def bounce?
+      @recipient.match(/-bounce@/) ||
+          # Empty Return-Path
+          self.return_path.to_s == '<>' ||
+          # Auto-Submitted exists and does not equal 'no'
+          ( self['Auto-Submitted'].present? && self['Auto-Submitted'].to_s.downcase != 'no' )
+    end
+
+    def keywords
+      return @keywords if @keywords
+
+      part = first_plaintext_part
+      if part.blank?
+        return []
+      end
+
+      @keywords = []
+      part.body = 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
+        end
+        # TODO: Find multiline arguments (add-key). Currently add-key has to
+        # read the whole body and hope for the best.
+        if line.match(/^x-([^: ]*)[: ]*(.*)/i)
+          command = $1.strip.downcase
+          arguments = $2.to_s.strip.downcase.split(/[,; ]{1,}/)
+          @keywords << [command, arguments]
+          nil
+        else
+          line
+        end
+      end.compact.join
+
+      @keywords
+    end
+
+    def add_subject_prefix(string)
+      if ! string.to_s.strip.empty?
+        prefix = "#{string} "
+        # Only insert prefix if it's not present already.
+        if ! self.subject.include?(prefix)
+          self.subject = "#{string} #{self.subject}"
+        end
+      end
+    end
+
+    def add_pseudoheader(key, value)
+      @dynamic_pseudoheaders ||= []
+      @dynamic_pseudoheaders << make_pseudoheader(key, value)
+    end
+
+    def make_pseudoheader(key, value)
+      "#{key.to_s.capitalize}: #{value.to_s}"
+    end
+
+    def dynamic_pseudoheaders
+      @dynamic_pseudoheaders || []
+    end
+
+    def standard_pseudoheaders(list)
+      if @standard_pseudoheaders.present?
+        return @standard_pseudoheaders
+      else
+        @standard_pseudoheaders = []
+      end
+
+      Array(list.headers_to_meta).each do |field|
+        @standard_pseudoheaders << make_pseudoheader(field.to_s, self.header[field.to_s])
+      end
+
+      # Careful to add information about the incoming signature. GPGME
+      # throws exceptions if it doesn't know the key.
+      if self.signature.present?
+        msg = begin
+                self.signature.to_s
+              rescue EOFError
+                "Unknown signature by 0x#{self.signature.fingerprint}"
+              end
+      else
+        msg = "Unsigned"
+      end
+      @standard_pseudoheaders << make_pseudoheader(:sig, msg)
+
+      @standard_pseudoheaders << make_pseudoheader(
+            :enc,
+            was_encrypted? ? 'Encrypted' : 'Unencrypted'
+        )
+
+      @standard_pseudoheaders
+    end
+
+    def pseudoheaders(list)
+      (standard_pseudoheaders(list) + dynamic_pseudoheaders).flatten.join("\n") + "\n"
+    end
+
+    def add_msgids(list, orig)
+      if list.keep_msgid
+        # Don't use `orig['in-reply-to']` here, because that sometimes fails to
+        # parse the original value and then returns it without the
+        # angle-brackets.
+        self.message_id = clutch_anglebrackets(orig.message_id)
+        self.in_reply_to = clutch_anglebrackets(orig.in_reply_to)
+        self.references = clutch_anglebrackets(orig.references)
+      end
+    end
+
+    def add_list_headers(list)
+      if list.include_list_headers
+        self['List-Id'] = "<#{list.email.gsub('@', '.')}>"
+        self['List-Owner'] = "<mailto:#{list.owner_address}> (Use list's public key)"
+        self['List-Help'] = '<https://schleuder2.nadir.org/>'
+
+        postmsg = if list.receive_admin_only
+                    "NO (Admins only)"
+                  elsif list.receive_authenticated_only
+                    "<mailto:#{list.email}> (Subscribers only)"
+                  else
+                    "<mailto:#{list.email}>"
+                  end
+
+        self['List-Post'] = postmsg
+      end
+    end
+
+    def add_openpgp_headers(list)
+      if list.include_openpgp_header
+
+        if list.openpgp_header_preference == 'none'
+          pref = ''
+        else
+          pref = "preference=#{list.openpgp_header_preference}"
+
+          # TODO: simplify.
+          pref << ' ('
+          if list.receive_admin_only
+            pref << 'Only encrypted and signed emails by list-admins are accepted'
+          elsif ! list.receive_authenticated_only
+            if list.receive_encrypted_only && list.receive_signed_only
+              pref << 'Only encrypted and signed emails are accepted'
+            elsif list.receive_encrypted_only && ! list.receive_signed_only
+              pref << 'Only encrypted emails are accepted'
+            elsif ! list.receive_encrypted_only && list.receive_signed_only
+              pref << 'Only signed emails are accepted'
+            else
+              pref << 'All kind of emails are accepted'
+            end
+          elsif list.receive_authenticated_only
+            if list.receive_encrypted_only
+              pref << 'Only encrypted and signed emails by subscribers are accepted'
+            else
+              pref << 'Only signed emails by subscribers are accepted'
+            end
+          else
+            pref << 'All kind of emails are accepted'
+          end
+          pref << ')'
+        end
+
+        fingerprint = list.key.fingerprint
+        comment = "(Send an email to #{list.sendkey_address} to receive the public-key)"
+
+        self['OpenPGP'] = "id=0x#{fingerprint} #{comment}; #{pref}"
+      end
+    end
+
+    def empty?
+      if self.multipart?
+        if self.parts.empty?
+          return true
+        else
+          # Test parts recursively. E.g. Thunderbird with activated
+          # memoryhole-headers send nested parts that might still be empty.
+          return self.parts.reduce { |result, part| result && part.empty? }
+        end
+      else
+        return self.body.empty?
+      end
+    end
+
+    private
+
+
+    def first_plaintext_part(part=nil)
+      part ||= self
+      if part.multipart?
+        first_plaintext_part(part.parts.first)
+      elsif part.mime_type == 'text/plain'
+        part
+      else
+        nil
+      end
+    end
+
+    def clutch_anglebrackets(input)
+      Array(input).map do |string|
+        if string.first == '<'
+          string
+        else
+          "<#{string}>"
+        end
+      end.join(' ')
+    end
+  end
+end
diff --git a/lib/schleuder/plugins/foo.rb b/lib/schleuder/plugins/foo.rb
new file mode 100644
index 0000000..6e1a8a5
--- /dev/null
+++ b/lib/schleuder/plugins/foo.rb
@@ -0,0 +1,8 @@
+module Schleuder
+  module ListPlugins
+    def self.foo(arguments, mail)
+      mail.add_pseudoheader :foo, 'Bar!'
+      nil
+    end
+  end
+end
diff --git a/lib/schleuder/plugins/key_management.rb b/lib/schleuder/plugins/key_management.rb
new file mode 100644
index 0000000..94f8ad8
--- /dev/null
+++ b/lib/schleuder/plugins/key_management.rb
@@ -0,0 +1,51 @@
+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}"
+      end
+    end
+
+    def self.delete_key(arguments, list, mail)
+      arguments.map do |argument|
+        # TODO: I18n
+        if list.gpg.delete(argument)
+          "Deleted: #{argument}."
+        else
+          "Not found: #{argument}."
+        end
+      end
+    end
+
+    def self.list_keys(arguments, list, mail)
+      args = arguments.presence || ['']
+      args.map do |argument|
+        list.keys(argument).map do |key|
+          key.to_s
+        end
+      end
+    end
+
+    def self.get_key(arguments, list, mail)
+      arguments.map do |argument|
+        list.export_key(argument)
+      end
+    end
+
+    def self.fetch_key(arguments, list, mail)
+      hkp = Hkp.new
+      arguments.map do |argument|
+        hkp.fetch_and_import(argument)
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/plugins/resend.rb b/lib/schleuder/plugins/resend.rb
new file mode 100644
index 0000000..1897138
--- /dev/null
+++ b/lib/schleuder/plugins/resend.rb
@@ -0,0 +1,74 @@
+module Schleuder
+  module ListPlugins
+    def self.resend(arguments, list, mail)
+      resend_it(arguments, list, mail, false)
+      # Return nil to prevent any erronous output to be interpreted as error.
+      nil
+    end
+
+    def self.resend_encrypted_only(arguments, list, mail)
+      resend_it(arguments, list, mail, true)
+      nil
+    end
+
+    def self.resend_it(arguments, list, mail, send_encrypted_only)
+      # If we must encrypt, first test if there's a key for every recipient.
+      found_keys = {}
+      arguments.each do |email|
+        keys = list.keys_by_email(email)
+        case keys.size
+        when 0
+          # TODO: I18n.
+          mail.add_pseudoheader(:note, "No key found for #{email}.")
+        when 1
+          found_keys[email] = keys.first
+        else
+          # TODO: I18n.
+          mail.add_pseudoheader(:note, "Multiple keys found for #{email}, not using any.")
+        end
+      end
+
+      if send_encrypted_only
+        missing = arguments - found_keys.keys
+        if missing.present?
+          return I18n.t("plugins.resend.not_resent_no_keys", emails: missing.join(', '))
+        end
+      end
+
+      arguments.map do |email|
+        # Setup encryption
+        gpg_opts = {sign: true}
+        if found_keys[email].present?
+          gpg_opts.merge!(encrypt: true)
+        end
+
+        # Compose and send email
+        new = mail.clean_copy
+        new.to = email
+
+        # Add public_footer unless it's empty?.
+        if ! list.public_footer.to_s.strip.empty?
+          footer_part = Mail::Part.new
+          footer_part.body = list.public_footer.strip
+          new.add_part footer_part
+        end
+
+        new.gpg gpg_opts
+        if new.deliver
+          mail.add_pseudoheader('resent-to', resent_pseudoheader(email, found_keys[email]))
+          mail.add_subject_prefix(list.subject_prefix_out)
+        end
+      end
+      # TODO: catch and handle SMTPFatalError (is raised when recipient is rejected by remote)
+    end
+
+    def self.resent_pseudoheader(email, key)
+      str = email
+      if key.present?
+        str << " (#{I18n.t('plugins.resend.encrypted_with')} #{key.fingerprint})"
+      else
+        str << " (#{I18n.t('plugins.resend.unencrypted')})"
+      end
+    end
+  end
+end
diff --git a/lib/schleuder/plugins/sign_this.rb b/lib/schleuder/plugins/sign_this.rb
new file mode 100644
index 0000000..a51a81a
--- /dev/null
+++ b/lib/schleuder/plugins/sign_this.rb
@@ -0,0 +1,52 @@
+module Schleuder
+  module RequestPlugins
+    def self.sign_this(arguments, list, mail)
+      if mail.parts.empty? && mail.body.to_s.present?
+        # Single text/plain-output is handled by the plugin-runner well, we
+        # don't need to take care of the reply.
+        list.logger.debug "Clear-signing text/plain body"
+        clearsign(mail)
+      else
+        # Here we need to send our reply manually because we're sending
+        # attachments. Maybe move this ability into the plugin-runner?
+        out = multipart(mail.reply, list, mail)
+        out.body = I18n.t('plugins.signatures_attached')
+        list.logger.info "Replying directly to sender"
+        mail.signer.send_mail(out)
+        list.logger.info "Exiting."
+        exit
+      end
+    end
+
+    def self.multipart(out, list, mail)
+      list.logger.debug "Signing each attachment's body"
+      mail.parts.each do |part|
+        next if part.body.to_s.strip.blank?
+        file_basename = part.filename.presence || Digest::SHA256.hexdigest(part.body.to_s)
+        list.logger.debug "Signing #{file_basename}"
+        filename = "#{file_basename}.sig"
+        out.add_file({
+            filename: filename,
+            content: detachsign(part.body.to_s)
+          })
+        out.attachments[filename].content_description = "OpenPGP signature for '#{file_basename}'"
+      end
+      out
+    end
+
+    def self.sign_each_part(list, mail)
+    end
+
+    def self.detachsign(thing)
+      crypto.sign(thing, mode: GPGME::SIG_MODE_DETACH).to_s
+    end
+
+    def self.clearsign(mail)
+      return crypto.clearsign(mail.body.to_s).to_s
+    end
+
+    def self.crypto
+      @crypto ||= GPGME::Crypto.new
+    end
+  end
+end
diff --git a/lib/schleuder/plugins/subscription_management.rb b/lib/schleuder/plugins/subscription_management.rb
new file mode 100644
index 0000000..7740b7a
--- /dev/null
+++ b/lib/schleuder/plugins/subscription_management.rb
@@ -0,0 +1,113 @@
+module Schleuder
+  module RequestPlugins
+    def self.subscribe(arguments, list, mail)
+      sub = list.subscriptions.new(
+        email: arguments.first,
+        fingerprint: arguments.last
+      )
+
+      if sub
+        I18n.t(
+          "plugins.subscription_management.subscribed",
+          email: sub.email,
+          fingerprint: sub.fingerprint
+        )
+      else
+        I18n.t(
+          "plugins.subscription_management.subscribing_failed",
+          email: sub.email,
+          errors: sub.errors.full_messages
+        )
+      end
+    end
+
+    def self.unsubscribe(arguments, list, mail)
+      # If no address was given we unsubscribe the sender.
+      email = arguments.first.presence || mail.signer.email
+
+      # TODO: May signers have multiple UIDs? We don't match those currently.
+      if ! list.from_admin?(mail) && email != mail.signer.email
+        # Only admins may unsubscribe others.
+        return I18n.t(
+          "plugins.subscription_management.forbidden", email: email
+        )
+      end
+
+      sub = list.subscriptions.where(email: email).first
+
+      if sub.blank?
+        return I18n.t(
+          "plugins.subscription_management.is_not_subscribed", email: email
+        )
+      end
+
+      if res = sub.delete
+        I18n.t(
+          "plugins.subscription_management.unsubscribed", email: email
+        )
+      else
+        I18n.t(
+          "plugins.subscription_management.unsubscribing_failed",
+          email: email,
+          error: res.errors.to_a
+        )
+      end
+    end
+
+    def self.list_subscriptions(arguments, list, mail)
+      out = [
+        "#{I18n.t("plugins.subscription_management.list_of_subscriptions")}:"
+      ]
+
+      subs = if arguments.blank?
+                list.subscriptions.all.to_a
+             else
+               arguments.map do |argument|
+                 list.subscriptions.where("email like ?", "%#{argument}%").to_a
+               end.flatten
+             end
+
+      out << subs.map do |subscription|
+        # Fingerprints are at most 40 characters long, and lines shouldn't
+        # exceed 80 characters if possible.
+        "#{subscription.email.rjust(37)} 0x#{subscription.fingerprint}"
+      end
+    end
+
+    def self.set_fingerprint(arguments, list, mail)
+      email = if list.admin?(mail.signer.email)
+                arguments.first
+              else
+                # TODO: send error message if signer tried to set another
+                # fingerprint than hir own.
+                mail.signer.email
+              end
+
+      sub = list.subscriptions.where(email: mail.signer.email)
+
+      if sub.blank?
+        return I18n.t(
+          "plugins.subscription_management.is_not_subscribed", email: email
+        )
+      end
+
+      sub.fingerprint = arguments.last
+
+      if sub.save
+        I18n.t(
+          "plugins.subscription_management.fingerprint_set",
+          email: email,
+          fingerprint: sub.fingerprint
+        )
+      else
+        I18n.t(
+          "plugins.subscription_management.setting_fingerprint_failed",
+          email: email,
+          fingerprint: arguments.last,
+          error: sub.errors.to_a.join("\n")
+        )
+      end
+    end
+  end
+end
+
diff --git a/lib/schleuder/plugins_runner.rb b/lib/schleuder/plugins_runner.rb
new file mode 100644
index 0000000..f7ef3bd
--- /dev/null
+++ b/lib/schleuder/plugins_runner.rb
@@ -0,0 +1,60 @@
+module Schleuder
+  module Plugins
+    class Runner
+      def self.run(list, mail)
+        list.logger.debug "Starting plugins runner"
+        @list = list
+        @mail = mail
+        setup
+        output = []
+        if @mail.request?
+          @plugin_module = RequestPlugins
+        else
+          @plugin_module = ListPlugins
+        end
+        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)
+            next
+          end
+          output << run_plugin(keyword, arguments)
+        end
+        output
+      end
+
+      def self.run_plugin(keyword, arguments)
+        command = keyword.gsub('-', '_')
+        if @plugin_module.respond_to?(command)
+          out = @plugin_module.send(command, arguments, @list, @mail)
+          response = Array(out).flatten.join("\n\n")
+          if @list.keywords_admin_notify.include?(keyword)
+            explanation = I18n.t('plugins.keyword_admin_notify', 
+                                    signer: @mail.signer,
+                                    keyword: keyword,
+                                    response: response
+                                )
+            @list.logger.notify_admin("#{explanation}\n\n#{response}\n", nil, 'Notice')
+          end
+          response
+        else
+          I18n.t('plugins.unknown_keyword', keyword: keyword)
+        end
+      rescue => exc
+        # Log to system, this information is probably more useful for
+        # system-admins than for list-admins.
+        Schleuder.logger.error(exc.message_with_backtrace)
+        I18n.t("plugins.plugin_failed", keyword: keyword)
+      end
+
+      def self.setup
+        @list.logger.debug "Loading plugins"
+        Dir["#{Schleuder::Conf.plugins_dir}/*.rb"].each do |file|
+          require file
+        end
+      end
+    end
+
+  end
+end
diff --git a/lib/schleuder/runner.rb b/lib/schleuder/runner.rb
new file mode 100644
index 0000000..bf216a3
--- /dev/null
+++ b/lib/schleuder/runner.rb
@@ -0,0 +1,127 @@
+module Schleuder
+  class Runner
+    def run(msg, recipient)
+      error = setup_list(recipient)
+      return error if error
+
+      logger.info "Parsing incoming email."
+      begin
+        # This decrypts, verifies, etc.
+        @mail = Mail.new(msg)
+        @mail = @mail.setup(recipient, list)
+      rescue GPGME::Error::DecryptFailed
+        logger.warn "Decryption of incoming message failed."
+        return Errors::DecryptionFailed.new(list)
+      end
+
+      # Filters
+      error = Filters::Runner.run(list, @mail)
+      if error
+        if list.bounces_notify_admins?
+          text = "#{I18n.t('.bounces_notify_admins')}\n\n#{error}"
+          # TODO: raw_source is mostly blank?
+          logger.notify_admin text, @mail.original_message, I18n.t('notice')
+        end
+        return error
+      end
+
+      if ! @mail.was_encrypted?
+        logger.debug "Message was not encrypted, skipping plugins"
+      else
+        logger.debug "Message was encrypted."
+        if ! @mail.was_validly_signed?
+          logger.debug "Message was not validly signed, adding subject_prefix_in and skipping plugins"
+          @mail.add_subject_prefix(list.subject_prefix_in)
+        else
+          # Plugins
+          logger.debug "Message was encrypted and validly signed"
+          output = Plugins::Runner.run(list, @mail).compact
+
+          # Any output will be treated as error-message. Text meant for users
+          # should have been put into the mail by the plugin.
+          output.each do |something|
+            @mail.add_pseudoheader(:error, something.to_s) if something.present?
+          end
+        end
+      end
+
+      # Don't send empty messages over the list.
+      if @mail.empty?
+        logger.info "Message found empty, not sending it to list."
+        return Errors::MessageEmpty.new(@list)
+      end
+
+      logger.debug "Adding subject_prefix"
+      @mail.add_subject_prefix(list.subject_prefix)
+
+      # Subscriptions
+      send_to_subscriptions
+      nil
+    end
+
+    private
+
+    def send_to_subscriptions
+      logger.debug "Sending to subscriptions."
+      logger.debug "Creating clean copy of message"
+      new = @mail.clean_copy(true)
+      list.subscriptions.each do |subscription|
+        begin
+          subscription.send_mail(new)
+        rescue => exc
+          logger.error exc
+        end
+      end
+    end
+
+    def list
+      @list
+    end
+
+    def logger
+      list.present? && list.logger || Schleuder.logger
+    end
+
+    def log_and_return(error)
+      Schleuder.logger.error(error)
+      error
+    end
+
+    def setup_list(recipient)
+      return @list if @list
+
+      logger.info "Loading list '#{recipient}'"
+      if ! @list = List.by_recipient(recipient)
+        return log_and_return(Errors::ListNotFound.new(recipient))
+      end
+
+      # Check basic sanity of list.
+      %w[fingerprint key secret_key admins].each do |attrib|
+        if @list.send(attrib).blank?
+          return log_and_return(Errors::ListPropertyMissing.new(attrib))
+        end
+      end
+
+      # Check neccessary permissions of crucial files.
+      if ! File.readable?(@list.listdir)
+        return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_readable))
+      elsif ! File.directory?(@list.listdir)
+        return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_a_directory))
+      end
+      if ! File.writable?(@list.logfile)
+        return log_and_return(Errors::ListdirProblem.new(@list.logfile, :not_writable))
+      end
+
+      # Set locale
+      if I18n.available_locales.include?(@list.language.to_sym)
+        I18n.locale = @list.language.to_sym
+      end
+
+      # This cannot be put in List, as Mail wouldn't know it then.
+      logger.debug "Setting GNUPGHOME to #{@list.listdir}"
+      ENV['GNUPGHOME'] = @list.listdir
+      nil
+    end
+
+  end
+end
diff --git a/lib/schleuder/subscription.rb b/lib/schleuder/subscription.rb
new file mode 100644
index 0000000..dd1aeb6
--- /dev/null
+++ b/lib/schleuder/subscription.rb
@@ -0,0 +1,83 @@
+module Schleuder
+  class Subscription < ActiveRecord::Base
+    belongs_to :list
+
+    validates :list_id, inclusion: {
+                          in: -> (id) { List.pluck(:id) },
+                          message: "must refer to an existing list"
+                        }
+    validates :email, presence: true, email: true
+    validates :fingerprint, allow_blank: true, fingerprint: true
+    validates :delivery_enabled, :admin, boolean: true
+
+    default_scope { order(:email) }
+
+    def to_s
+      email
+    end
+
+    def self.configurable_attributes
+      [:fingerprint, :admin, :delivery_enabled]
+    end
+
+    def fingerprint=(arg)
+      # Strip whitespace from incoming arg.
+      write_attribute(:fingerprint, arg.to_s.gsub(/\s*/, '').chomp)
+    end
+
+    def key
+      # TODO: make key-related methods a concern, so we don't have to go
+      # through the list and neither re-implement the methods here.
+      # Prefix '0x' to force GnuPG to match only hex-values, not UIDs.
+      list.keys("0x#{self.fingerprint}").first
+    end
+
+    def send_mail(mail)
+      list.logger.debug "Preparing sending to #{self.inspect}"
+
+      if ! self.delivery_enabled
+        list.logger.info "Not sending to #{self.email}: delivery is disabled."
+        return false
+      end
+
+      mail = ensure_headers(mail)
+      gpg_opts = {encrypt: true, sign: true, keys: {self.email => "0x#{self.fingerprint}"}}
+      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
+          return false
+        else
+          gpg_opts.merge!(encrypt: false)
+        end
+      end
+      list.logger.info "Sending message to #{self.email}"
+      mail.gpg gpg_opts
+      mail.deliver
+    end
+
+    def ensure_headers(mail)
+      mail.to = self.email
+      mail.from = self.list.email
+      mail.return_path = self.list.bounce_address
+      mail
+    end
+
+    def notify_of_missed_message
+      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.gpg({encrypt: false, sign: true})
+      mail.deliver
+    end
+
+    def admin?
+      self.admin == true
+    end
+
+    def delete_key
+      list.delete_key(self.fingerprint)
+    end
+
+  end
+end
diff --git a/lib/schleuder/validators/boolean_validator.rb b/lib/schleuder/validators/boolean_validator.rb
new file mode 100644
index 0000000..a08bb1f
--- /dev/null
+++ b/lib/schleuder/validators/boolean_validator.rb
@@ -0,0 +1,7 @@
+class BooleanValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    if ! [true, false].include?(value)
+      record.errors.add(attribute, I18n.t("errors.must_be_boolean"))
+    end
+  end
+end
diff --git a/lib/schleuder/validators/email_validator.rb b/lib/schleuder/validators/email_validator.rb
new file mode 100644
index 0000000..2f26262
--- /dev/null
+++ b/lib/schleuder/validators/email_validator.rb
@@ -0,0 +1,7 @@
+class EmailValidator <  ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    unless value =~ Conf::EMAIL_REGEXP
+      record.errors[attribute] << (options[:message] || I18n.t("errors.invalid_email"))
+    end
+  end
+end
diff --git a/lib/schleuder/validators/fingerprint_validator.rb b/lib/schleuder/validators/fingerprint_validator.rb
new file mode 100644
index 0000000..02f0dbc
--- /dev/null
+++ b/lib/schleuder/validators/fingerprint_validator.rb
@@ -0,0 +1,7 @@
+class FingerprintValidator <  ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    unless value =~ /\A[a-f0-9]+\z/i
+      record.errors[attribute] << (options[:message] || I18n.t("errors.invalid_fingerprint"))
+    end
+  end
+end
diff --git a/lib/schleuder/validators/greater_than_zero_validator.rb b/lib/schleuder/validators/greater_than_zero_validator.rb
new file mode 100644
index 0000000..3eccd55
--- /dev/null
+++ b/lib/schleuder/validators/greater_than_zero_validator.rb
@@ -0,0 +1,7 @@
+class GreaterThanZeroValidator <  ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    if value.to_i == 0
+      record.errors.add(attribute, I18n.t("errors.must_be_greater_than_zero"))
+    end
+  end
+end
diff --git a/lib/schleuder/validators/no_line_breaks_validator.rb b/lib/schleuder/validators/no_line_breaks_validator.rb
new file mode 100644
index 0000000..1bcf663
--- /dev/null
+++ b/lib/schleuder/validators/no_line_breaks_validator.rb
@@ -0,0 +1,7 @@
+class NoLineBreaksValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    if value.to_s.include?("\n")
+      record.errors.add(attribute, I18n.t("errors.no_linebreaks") )
+    end
+  end
+end
diff --git a/lib/schleuder/version.rb b/lib/schleuder/version.rb
new file mode 100644
index 0000000..296bf25
--- /dev/null
+++ b/lib/schleuder/version.rb
@@ -0,0 +1,3 @@
+module Schleuder
+  VERSION = '3.0.0.beta7'
+end
diff --git a/locales/de.yml b/locales/de.yml
new file mode 100644
index 0000000..b559313
--- /dev/null
+++ b/locales/de.yml
@@ -0,0 +1,119 @@
+de:
+  errors:
+    attributes:
+      language:
+        inclusion: "muss einem der folgenden Werte entsprechen: en, de"
+      log_level:
+        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"
+      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!'
+    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!'
+    fatalerror: |
+      Es ist ein schwerwiegender Fehler aufgetreten. Administratoren wurden benachrichtigt.
+      Bitte versuche es später noch ein Mal.
+    signoff: |
+
+
+      Freundliche Grüße,
+      Dein Schleuder-System.
+    decryption_failed: |
+      Deine Email konnnte nicht entschlüsselt werden.
+      Emails an diese Adresse müssen mit diesem Schlüssel verschlüsselt werden:
+
+      %{key}
+
+      Um den Schlüssel zugesandt zu bekommen sende eine Email an
+      <%{sendkey_email}>.
+    message_unsigned: Emails an diese Adresse müssen mit einem OpenPGP-Schlüssel signiert sein.
+    message_signature_unknown: |
+      Emails an diese Adresse müssen mit dem OpenPGP-Schlüssel signiert sein, der für
+      dein Abo eingetragen ist. Wenn du nicht weisst, welcher Schlüssel das ist, frage
+      die Administrator/innen. Die erreichst du per Email an
+      <%{owner_email}>.
+      (Vorzugsweise verschlüssele die Email mit dem Schlüssel dieser Adresse:
+      %{list_fingerprint}).
+    message_unencrypted: Emails an diese Adresse müssen OpenPGP-konform verschlüsselt sein.
+    message_unauthenticated: Emails an diese Adresse müssen verschlüsselt und mit einem OpenPGP-Schlüssel signiert sein, der für ein Abo eingetragen ist.
+    message_sender_not_subscribed: Nur Absender mit Abo dürfen Emails an diese Adresse schicken.
+    message_not_from_admin: Nur Admins dürfen Emails an diese Adresse schicken.
+    message_empty: |
+      Deine Email enthielt keinen Text, daher wurde sie nicht über die Liste verteilt.
+
+      Falls du ausschließlich Schlüsselwörter gesendet hast beachte, dass administrative Schlüsselwörter an die "request"-Adresse (<%{request_address}>) geschickt werden müssen um berücksichtigt zu werden.
+    list_not_found: "Fehler: Keine Liste zu dieser Adresse gefunden: '%{email}'."
+    no_linebreaks: "Darf keine Zeilenumbrüche enthalten"
+    invalid_characters: "enthält ungültige Zeichen"
+    invalid_listname: "Fehler: '%{email}' ist kein gültige Listen-Adresse."
+    list_exists: Es existiert bereits eine Liste mit der Adresse '%{email}'.
+    listdir_problem:
+      message: "Problem mit dem Listen-Verzeichnis: '%{dir}' %{problem}."
+      not_a_directory: ist kein Verzeichnis
+      not_empty: ist nicht leer
+      not_writable: ist nicht beschreibbar
+      not_readable: ist nicht lesbar
+    keyword_admin_only: Das Schlüsselwort '%{keyword}' darf nur von Listen-Admins verwendet werden.
+    key_generation_failed: Das Erzeugen des OpenPGP-Schlüsselpaares für %{listname} ist aus unbekannten Gründen fehlgeschlagen. Bitte prüfe das Listen-Verzeichnis ('%{listdir}') und die Log-Dateien.
+    key_adduid_failed: "Das Hinzufügen einer User-ID zum OpenPGP-Schlüssel ist mit folgender Meldung fehlgeschlagen:\n%{errmsg}"
+    too_many_keys: "Fehler: In %{listdir} existieren mehrere OpenPGP-Schlüssel für %{listname}. Bitte lösche alle bis auf einen."
+    unknown_list_option: "Unbekannte Option in %{config_file}: '%{option}'."
+    loading_list_settings_failed: "%{config_file} konnte nicht eingelesen werden, bitte Formatierung auf gültiges YAML prüfen."
+    message_too_big: "Deine Email war zu groß. Erlaubt sind für diese Liste %{allowed_size}KB."
+    must_be_boolean: "muss true oder false sein"
+    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."
+  plugins:
+    unknown_keyword: Unbekanntes Schlüsselwort '%{keyword}'.
+    plugin_failed: Das Schlüsselwort '%{keyword}' verursachte einen unbekannten Fehler. Die System-Administratoren wurden benachrichtigt.
+    keyword_admin_notify: "%{signer} schickte das Schlüsselwort '%{keyword}' und erhielt dies als Antwort:"
+    key_management:
+      import_result: "Import-Ergebnis:"
+      key_import_status:
+        imported: importiert
+        updated: aktualisiert
+        unchanged: unverändert
+    resend:
+      not_resent_no_keys: "Das Versenden schlug fehl. Für die folgenden Adressen wurde kein Schlüssel gefunden und unverschlüsseltes Senden war untersagt: %{emails}."
+      encrypted_with: verschlüsselt mit
+      unencrypted: unverschlüsselt
+    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.
+      unsubscribed: Abo für %{email} wurde gelöscht.
+      unsubscribing_failed: |
+        Abo für %{email} nicht gelöscht:
+        %{errors}
+      subscribed: Abo für %{email} mit Fingerabdruck %{fingerprint} eingetragen.
+      subscribing_failed: |
+        Abo für %{email} nicht eingetragen:
+        %{errors}
+      list_of_subscriptions: Abos
+      fingerprint_set: Fingerabdruck für %{email} auf %{fingerprint} gesetzt.
+      setting_fingerprint_failed: |
+        Fingerabdruck für %{email} konnte nicht auf %{fingerprint} gesetzt werden:
+        %{errors}.
+    signatures_attached: Die Signaturen hängen an.
+  list_public_key_attached: Der Schlüssel zu dieser Adresse hängt an.
+  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.
+  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:"
+  notice: Hinweis
+  incoming_message: Eingehende Email
+  forward_all_incoming_to_admins: Die angehängte Email ging ein.
+  forward_bounce_to_admins: Die angehängte Email ging als zurückgewiesen (bounce) ein.
+  bounce: Zurückgewiesene Nachricht
+  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."
diff --git a/locales/en.yml b/locales/en.yml
new file mode 100644
index 0000000..944997c
--- /dev/null
+++ b/locales/en.yml
@@ -0,0 +1,119 @@
+en:
+  errors:
+    attributes:
+      language:
+        inclusion: "must be one of: en, de"
+      log_level:
+        inclusion: "must be one of: debug, info, warn, error"
+      openpgp_header_preference:
+        inclusion: "must be one of: sign, encrypt, signencrypt, unprotected, none"
+      public_footer:
+        invalid: "includes non-printable characters"
+    invalid_email: "is not a valid email address"
+    invalid_fingerprint: "is not a valid 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!'
+    list_admins_missing: "List has no admins configured, cannot run!"
+    fatalerror: |
+      A fatal error happened. Administrators have been notified.
+      Please try again later.
+    signoff: |
+
+
+      Kind regards,
+      Your Schleuder system.
+    decryption_failed: |
+      Decrypting your message failed.
+      Messages to this address must be encrypted with the following key:
+
+      %{key}
+
+      To receive it send an email to
+      <%{email}>.
+    message_unsigned: Messages to this address must be OpenPGP-signed.
+    message_signature_unknown: |
+      Messages to this address must be OpenPGP-signed by the key that is configured
+      for your subscription. If you don't know which one that is, ask an administrator
+      of this list. You can contact the administrators by sending a message to
+      <%{owner_email}>
+      (preferably encrypt it with this addresses' public key:
+      %{list_fingerprint}).
+    message_unencrypted: Messages to this address must be encrypted conforming to OpenPGP.
+    message_unauthenticated: Messages to this address must be encrypted and signed by the key associated with a subscribed address.
+    message_sender_not_subscribed: Only subscribed addresses may send messages to this address.
+    message_not_from_admin: Only admins may send messages to this address.
+    message_empty: |
+      Your message was found empty and wasn't passed on to the list.
+
+      In case you only sent keywords please note that administrative keywords must be sent to the "request"-address (<%{request_address}>) in order to be respected.
+    no_linebreaks: "must not include line-breaks"
+    list_not_found: "Error: No list found with this address: '%{email}'."
+    invalid_listname: "Error: '%{email}' is not a valid address."
+    invalid_characters: "contains invalid characters"
+    list_exists: A list with address '%{email}' is already present.
+    listdir_problem:
+      message: "There's a problem with the list-directory: '%{dir}' %{problem}."
+      not_a_directory: is not a directory
+      not_empty: is not empty
+      not_writable: is not writable
+      not_readable: is not readable
+    keyword_admin_only: The keyword '%{keyword}' may only be used by list-admin.
+    key_generation_failed: Generating the OpenPGP key pair for %{listname} failed for unknown reasons. Please check the list-directory ('%{listdir}') and the log-files.
+    key_adduid_failed: "Adding a user-ID to the OpenPGP key failed with this message:\n%{errmsg}"
+    too_many_keys: "Error: In %{listdir} there's more than one matching OpenPGP-key for %{listname}. Please delete all but one."
+    unknown_list_option: "Unknown option in %{config_file}: '%{option}'."
+    loading_list_settings_failed: "%{config_file} could not be parsed, please check its formatting to be valid YAML."
+    message_too_big: "Your message was too big. Allowed are up to %{allowed_size}KB."
+    must_be_boolean: "must be true or false"
+    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."
+  plugins:
+    unknown_keyword: Unknown keyword '%{keyword}'.
+    plugin_failed: Running keyword '%{keyword}' caused an unknown error. System-admins have been notified.
+    keyword_admin_notify: "%{signer} sent keyword '%{keyword}' and received this response:"
+    key_management:
+      import_result: "Import result:"
+      key_import_status:
+        imported: imported
+        updated: updated
+        unchanged: unchanged
+    resend:
+      not_resent_no_keys: "Resending failed. For the following recipients no matching key was found and unencrypted sending was disallowed: %{emails}."
+      encrypted_with: encrypted with
+      unencrypted: unencrypted
+    subscription_management:
+      forbidden: "Error: You're not allowed to unsubscribe %{email}."
+      is_not_subscribed: "%{email} is not subscribed."
+      unsubscribed: "%{email} has been unsubscribed."
+      unsubscribing_failed: |
+        Unsubscribing %{email} failed:
+        %{errors}
+      subscribed: "%{email} has been subscribed with fingerprint %{fingerprint}."
+      subscribing_failed: |
+        Subscribing %{email} failed:
+        %{errors}
+      list_of_subscriptions: Subscriptions
+      fingerprint_set: Fingerprint for %{email} set to %{fingerprint}.
+      setting_fingerprint_failed: |
+        Setting fingerprint for %{email} to %{fingerprint} failed:
+        %{errors}.
+    signatures_attached: Find the signatures attached.
+  list_public_key_attached: Find the key for this address attached.
+  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.
+  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:"
+  notice: Notice
+  incoming_message: Incoming message
+  forward_all_incoming_to_admins: The attached message was received.
+  forward_bounce_to_admins: The attached message was received as bounce.
+  bounce: Bounced message
+  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."
diff --git a/schleuder.gemspec b/schleuder.gemspec
new file mode 100644
index 0000000..96b3d89
--- /dev/null
+++ b/schleuder.gemspec
@@ -0,0 +1,67 @@
+#########################################################
+# This file has been automatically generated by gem2tgz #
+#########################################################
+# -*- encoding: utf-8 -*-
+# stub: schleuder 3.0.0.beta7 ruby lib
+
+Gem::Specification.new do |s|
+  s.name = "schleuder"
+  s.version = "3.0.0.beta7"
+
+  s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
+  s.require_paths = ["lib"]
+  s.authors = ["lunar", "ng", "paz"]
+  s.date = "2016-11-23"
+  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.email = "schleuder2 at nadir.org"
+  s.executables = ["schleuder", "schleuder-api-daemon"]
+  s.files = ["README.md", "Rakefile", "bin/schleuder", "bin/schleuder-api-daemon", "db/schema.rb", "etc/list-defaults.yml", "etc/schleuder-api-daemon.service", "etc/schleuder.yml", "lib/schleuder.rb", "lib/schleuder/cli.rb", "lib/schleuder/cli/cert.rb", "lib/schleuder/cli/schleuder_cert_manager.rb", "lib/schleuder/cli/subcommand_fix.rb", "lib/schleuder/conf.rb", "lib/schleuder/errors/active_model_error.rb", "lib/schleuder/errors/base.rb", "lib/schleuder/errors/decryption_failed.rb", "lib [...]
+  s.homepage = "http://schleuder.nadir.org/"
+  s.licenses = ["GPL-3.0"]
+  s.post_install_message = "\n\n    Please consider additionallly installing schleuder-conf (allows to\n    configure lists from the command line).\n\n    To set up Schleuder on this system please run `schleuder install`.\n\n  "
+  s.rubyforge_project = "[none]"
+  s.rubygems_version = "2.5.1"
+  s.summary = "Schleuder is a gpg-enabled mailinglist with remailing-capabilities."
+
+  if s.respond_to? :specification_version then
+    s.specification_version = 4
+
+    if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
+      s.add_runtime_dependency(%q<activerecord>, ["~> 4.1"])
+      s.add_development_dependency(%q<database_cleaner>, [">= 0"])
+      s.add_development_dependency(%q<hirb>, [">= 0"])
+      s.add_runtime_dependency(%q<mail-gpg>, ["~> 0.2.7"])
+      s.add_runtime_dependency(%q<rake>, ["~> 10"])
+      s.add_development_dependency(%q<rspec>, ["~> 3.5.0"])
+      s.add_runtime_dependency(%q<sinatra>, ["~> 1"])
+      s.add_runtime_dependency(%q<sinatra-contrib>, ["~> 1"])
+      s.add_runtime_dependency(%q<sqlite3>, ["~> 1"])
+      s.add_development_dependency(%q<thin>, [">= 0"])
+      s.add_runtime_dependency(%q<thor>, ["~> 0"])
+    else
+      s.add_dependency(%q<activerecord>, ["~> 4.1"])
+      s.add_dependency(%q<database_cleaner>, [">= 0"])
+      s.add_dependency(%q<hirb>, [">= 0"])
+      s.add_dependency(%q<mail-gpg>, ["~> 0.2.7"])
+      s.add_dependency(%q<rake>, ["~> 10"])
+      s.add_dependency(%q<rspec>, ["~> 3.5.0"])
+      s.add_dependency(%q<sinatra>, ["~> 1"])
+      s.add_dependency(%q<sinatra-contrib>, ["~> 1"])
+      s.add_dependency(%q<sqlite3>, ["~> 1"])
+      s.add_dependency(%q<thin>, [">= 0"])
+      s.add_dependency(%q<thor>, ["~> 0"])
+    end
+  else
+    s.add_dependency(%q<activerecord>, ["~> 4.1"])
+    s.add_dependency(%q<database_cleaner>, [">= 0"])
+    s.add_dependency(%q<hirb>, [">= 0"])
+    s.add_dependency(%q<mail-gpg>, ["~> 0.2.7"])
+    s.add_dependency(%q<rake>, ["~> 10"])
+    s.add_dependency(%q<rspec>, ["~> 3.5.0"])
+    s.add_dependency(%q<sinatra>, ["~> 1"])
+    s.add_dependency(%q<sinatra-contrib>, ["~> 1"])
+    s.add_dependency(%q<sqlite3>, ["~> 1"])
+    s.add_dependency(%q<thin>, [">= 0"])
+    s.add_dependency(%q<thor>, ["~> 0"])
+  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