[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