schleuder 2.2.4 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +138 -0
- data/Rakefile +136 -0
- data/bin/pinentry-clearpassphrase +72 -0
- data/bin/schleuder +9 -89
- data/bin/schleuder-api-daemon +4 -0
- data/db/migrate/20140501103532_create_lists.rb +39 -0
- data/db/migrate/20140501112859_create_subscriptions.rb +21 -0
- data/db/migrate/201508092100_add_language_to_lists.rb +11 -0
- data/db/migrate/20150812165700_change_keywords_admin_only_defaults.rb +8 -0
- data/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb +11 -0
- data/db/migrate/201508141727_change_send_encrypted_only_default.rb +8 -0
- data/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb +11 -0
- data/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb +14 -0
- data/db/migrate/201508261815_strip_gpg_passphrase.rb +11 -0
- data/db/migrate/201508261827_remove_default_mime.rb +9 -0
- data/db/migrate/20160501172700_fix_headers_to_meta_defaults.rb +8 -0
- data/db/migrate/20170713215059_add_internal_footer_to_list.rb +11 -0
- data/db/schema.rb +62 -0
- data/etc/init.d/schleuder-api-daemon +87 -0
- data/etc/list-defaults.yml +123 -0
- data/etc/postfix/schleuder_sqlite.cf +28 -0
- data/etc/schleuder-api-daemon.service +10 -0
- data/etc/schleuder.cron.weekly +6 -0
- data/etc/schleuder.yml +61 -0
- data/lib/schleuder-api-daemon.rb +420 -0
- data/lib/schleuder.rb +81 -47
- data/lib/schleuder/cli.rb +334 -0
- data/lib/schleuder/cli/cert.rb +24 -0
- data/lib/schleuder/cli/schleuder_cert_manager.rb +84 -0
- data/lib/schleuder/cli/subcommand_fix.rb +11 -0
- data/lib/schleuder/conf.rb +131 -0
- data/lib/schleuder/errors/active_model_error.rb +15 -0
- data/lib/schleuder/errors/base.rb +17 -0
- data/lib/schleuder/errors/decryption_failed.rb +16 -0
- data/lib/schleuder/errors/fatal_error.rb +13 -0
- data/lib/schleuder/errors/file_not_found.rb +14 -0
- data/lib/schleuder/errors/invalid_listname.rb +13 -0
- data/lib/schleuder/errors/key_adduid_failed.rb +13 -0
- data/lib/schleuder/errors/key_generation_failed.rb +16 -0
- data/lib/schleuder/errors/keyword_admin_only.rb +13 -0
- data/lib/schleuder/errors/list_exists.rb +13 -0
- data/lib/schleuder/errors/list_not_found.rb +14 -0
- data/lib/schleuder/errors/list_property_missing.rb +14 -0
- data/lib/schleuder/errors/listdir_problem.rb +16 -0
- data/lib/schleuder/errors/loading_list_settings_failed.rb +14 -0
- data/lib/schleuder/errors/message_empty.rb +14 -0
- data/lib/schleuder/errors/message_not_from_admin.rb +13 -0
- data/lib/schleuder/errors/message_sender_not_subscribed.rb +13 -0
- data/lib/schleuder/errors/message_too_big.rb +14 -0
- data/lib/schleuder/errors/message_unauthenticated.rb +13 -0
- data/lib/schleuder/errors/message_unencrypted.rb +13 -0
- data/lib/schleuder/errors/message_unsigned.rb +13 -0
- data/lib/schleuder/errors/standard_error.rb +5 -0
- data/lib/schleuder/errors/too_many_keys.rb +17 -0
- data/lib/schleuder/errors/unknown_list_option.rb +14 -0
- data/lib/schleuder/filters/auth_filter.rb +39 -0
- data/lib/schleuder/filters/bounces_filter.rb +12 -0
- data/lib/schleuder/filters/forward_filter.rb +17 -0
- data/lib/schleuder/filters/forward_incoming.rb +13 -0
- data/lib/schleuder/filters/hotmail_message_filter.rb +25 -0
- data/lib/schleuder/filters/max_message_size.rb +14 -0
- data/lib/schleuder/filters/request_filter.rb +26 -0
- data/lib/schleuder/filters/send_key_filter.rb +20 -0
- data/lib/schleuder/filters/strip_alternative_filter.rb +21 -0
- data/lib/schleuder/filters_runner.rb +83 -0
- data/lib/schleuder/gpgme/ctx.rb +274 -0
- data/lib/schleuder/gpgme/import_status.rb +27 -0
- data/lib/schleuder/gpgme/key.rb +212 -0
- data/lib/schleuder/gpgme/sub_key.rb +13 -0
- data/lib/schleuder/gpgme/user_id.rb +22 -0
- data/lib/schleuder/list.rb +318 -127
- data/lib/schleuder/list_builder.rb +139 -0
- data/lib/schleuder/listlogger.rb +31 -0
- data/lib/schleuder/logger.rb +23 -0
- data/lib/schleuder/logger_notifications.rb +69 -0
- data/lib/schleuder/mail/message.rb +482 -0
- data/lib/schleuder/mail/parts_list.rb +9 -0
- data/lib/schleuder/plugin_runners/base.rb +91 -0
- data/lib/schleuder/plugin_runners/list_plugins_runner.rb +24 -0
- data/lib/schleuder/plugin_runners/request_plugins_runner.rb +27 -0
- data/lib/schleuder/plugins/attach_listkey.rb +17 -0
- data/lib/schleuder/plugins/get_version.rb +7 -0
- data/lib/schleuder/plugins/key_management.rb +113 -0
- data/lib/schleuder/plugins/list_management.rb +15 -0
- data/lib/schleuder/plugins/resend.rb +196 -0
- data/lib/schleuder/plugins/sign_this.rb +46 -0
- data/lib/schleuder/plugins/subscription_management.rb +140 -0
- data/lib/schleuder/runner.rb +130 -0
- data/lib/schleuder/subscription.rb +98 -0
- data/lib/schleuder/validators/boolean_validator.rb +7 -0
- data/lib/schleuder/validators/email_validator.rb +7 -0
- data/lib/schleuder/validators/fingerprint_validator.rb +7 -0
- data/lib/schleuder/validators/greater_than_zero_validator.rb +7 -0
- data/lib/schleuder/validators/no_line_breaks_validator.rb +7 -0
- data/lib/schleuder/version.rb +1 -1
- data/locales/de.yml +179 -0
- data/locales/en.yml +179 -0
- metadata +305 -108
- checksums.yaml.gz.sig +0 -3
- data.tar.gz.sig +0 -2
- data/LICENSE +0 -339
- data/README +0 -32
- data/bin/schleuder-fix-gem-dependencies +0 -37
- data/bin/schleuder-init-setup +0 -37
- data/bin/schleuder-migrate-v2.1-to-v2.2 +0 -225
- data/bin/schleuder-newlist +0 -413
- data/contrib/check-expired-keys.rb +0 -60
- data/contrib/mutt-schleuder-colors.rc +0 -10
- data/contrib/mutt-schleuder-resend.vim +0 -24
- data/contrib/smtpserver.rb +0 -76
- data/ext/default-list.conf +0 -149
- data/ext/default-members.conf +0 -7
- data/ext/list.conf.example +0 -14
- data/ext/schleuder.conf +0 -64
- data/lib/schleuder/archiver.rb +0 -46
- data/lib/schleuder/crypt.rb +0 -210
- data/lib/schleuder/errors.rb +0 -5
- data/lib/schleuder/list_config.rb +0 -146
- data/lib/schleuder/log/listlogger.rb +0 -57
- data/lib/schleuder/log/outputter/emailoutputter.rb +0 -120
- data/lib/schleuder/log/outputter/metaemailoutputter.rb +0 -50
- data/lib/schleuder/log/schleuderlogger.rb +0 -34
- data/lib/schleuder/mail.rb +0 -873
- data/lib/schleuder/mailer.rb +0 -26
- data/lib/schleuder/member.rb +0 -69
- data/lib/schleuder/plugin.rb +0 -54
- data/lib/schleuder/processor.rb +0 -363
- data/lib/schleuder/schleuder_config.rb +0 -75
- data/lib/schleuder/storage.rb +0 -84
- data/lib/schleuder/utils.rb +0 -80
- data/man/schleuder-newlist.8 +0 -174
- data/man/schleuder.8 +0 -416
- data/plugins/README +0 -20
- data/plugins/manage_keys_plugin.rb +0 -113
- data/plugins/manage_members_plugin.rb +0 -156
- data/plugins/manage_self_plugin.rb +0 -26
- data/plugins/resend_plugin.rb +0 -35
- data/plugins/sign_this_plugin.rb +0 -14
- data/plugins/version_plugin.rb +0 -12
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module Schleuder
|
|
2
|
+
class ListBuilder
|
|
3
|
+
def initialize(list_attributes, adminemail=nil, adminfingerprint=nil, adminkey=nil)
|
|
4
|
+
@list_attributes = list_attributes.with_indifferent_access
|
|
5
|
+
@listname = list_attributes[:email]
|
|
6
|
+
@fingerprint = list_attributes[:fingerprint]
|
|
7
|
+
@adminemail = adminemail
|
|
8
|
+
@adminfingerprint = adminfingerprint
|
|
9
|
+
@adminkey = adminkey
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def read_default_settings
|
|
13
|
+
hash = YAML.load_file(ENV['SCHLEUDER_LIST_DEFAULTS'])
|
|
14
|
+
if ! hash.kind_of?(Hash)
|
|
15
|
+
raise Errors::LoadingListSettingsFailed.new
|
|
16
|
+
end
|
|
17
|
+
hash
|
|
18
|
+
rescue Psych::SyntaxError
|
|
19
|
+
raise Errors::LoadingListSettingsFailed.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run
|
|
23
|
+
Schleuder.logger.info "Building new list"
|
|
24
|
+
|
|
25
|
+
if @listname.blank? || ! @listname.match(Conf::EMAIL_REGEXP)
|
|
26
|
+
return [nil, {'email' => ["'#{@listname}' is not a valid email address"]}]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
settings = read_default_settings.merge(@list_attributes)
|
|
30
|
+
list = List.new(settings)
|
|
31
|
+
|
|
32
|
+
@list_dir = list.listdir
|
|
33
|
+
create_or_test_dir(@list_dir)
|
|
34
|
+
# In case listlogs_dir != lists_dir we have to create the basedir of the
|
|
35
|
+
# list's log-file.
|
|
36
|
+
create_or_test_dir(File.dirname(list.logfile))
|
|
37
|
+
|
|
38
|
+
if list.fingerprint.blank?
|
|
39
|
+
list_key = gpg.keys("<#{list.email}>").first
|
|
40
|
+
if list_key.nil?
|
|
41
|
+
list_key = create_key(list)
|
|
42
|
+
end
|
|
43
|
+
list.fingerprint = list_key.fingerprint
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if ! list.valid?
|
|
47
|
+
return list
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
list.save!
|
|
51
|
+
|
|
52
|
+
if @adminemail.blank?
|
|
53
|
+
msg = nil
|
|
54
|
+
else
|
|
55
|
+
sub, msg = list.subscribe(@adminemail, @adminfingerprint, true, true, @adminkey)
|
|
56
|
+
if sub.errors.present?
|
|
57
|
+
raise Errors::ActiveModelError.new(sub.errors)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[list, msg]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def gpg
|
|
65
|
+
@gpg_ctx ||= begin
|
|
66
|
+
ENV["GNUPGHOME"] = @list_dir
|
|
67
|
+
GPGME::Ctx.new
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_key(list)
|
|
72
|
+
Schleuder.logger.info "Generating key-pair, this could take a while..."
|
|
73
|
+
gpg.generate_key(key_params(list))
|
|
74
|
+
|
|
75
|
+
# Get key without knowing the fingerprint yet.
|
|
76
|
+
keys = list.keys(@listname)
|
|
77
|
+
if keys.empty?
|
|
78
|
+
raise Errors::KeyGenerationFailed.new(@list_dir, @listname)
|
|
79
|
+
elsif keys.size > 1
|
|
80
|
+
raise Errors::TooManyKeys.new(@list_dir, @listname)
|
|
81
|
+
else
|
|
82
|
+
adduids(list, keys.first)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
keys.first
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def adduids(list, key)
|
|
89
|
+
# Add UIDs for -owner and -request.
|
|
90
|
+
[list.request_address, list.owner_address].each do |address|
|
|
91
|
+
err = key.adduid(list.email, address)
|
|
92
|
+
if err.present?
|
|
93
|
+
raise err
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
# Go through list.key() to re-fetch the key from the keyring, otherwise
|
|
97
|
+
# we don't see the new UIDs.
|
|
98
|
+
errors = list.key.set_primary_uid(list.email)
|
|
99
|
+
if errors.present?
|
|
100
|
+
raise errors
|
|
101
|
+
end
|
|
102
|
+
rescue => exc
|
|
103
|
+
raise Errors::KeyAdduidFailed.new(exc.to_s)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def key_params(list)
|
|
107
|
+
"
|
|
108
|
+
<GnupgKeyParms format=\"internal\">
|
|
109
|
+
Key-Type: RSA
|
|
110
|
+
Key-Length: 4096
|
|
111
|
+
Key-Usage: sign
|
|
112
|
+
Subkey-Type: RSA
|
|
113
|
+
Subkey-Length: 4096
|
|
114
|
+
Subkey-Usage: encrypt
|
|
115
|
+
Name-Real: #{list.email}
|
|
116
|
+
Name-Email: #{list.email}
|
|
117
|
+
Expire-Date: 0
|
|
118
|
+
%no-protection
|
|
119
|
+
</GnupgKeyParms>
|
|
120
|
+
|
|
121
|
+
"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def create_or_test_dir(dir)
|
|
125
|
+
if File.exists?(dir)
|
|
126
|
+
if ! File.directory?(dir)
|
|
127
|
+
raise Errors::ListdirProblem.new(dir, :not_a_directory)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if ! File.writable?(dir)
|
|
131
|
+
raise Errors::ListdirProblem.new(dir, :not_writable)
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
FileUtils.mkdir_p(dir)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Schleuder
|
|
2
|
+
class Listlogger < ::Logger
|
|
3
|
+
include LoggerNotifications
|
|
4
|
+
def initialize(list)
|
|
5
|
+
super(list.logfile, 'daily')
|
|
6
|
+
@from = list.email
|
|
7
|
+
@list = list
|
|
8
|
+
@adminaddresses = list.admins.map { |sub| [sub.email, sub.key] }
|
|
9
|
+
@level = ::Logger.const_get(list.log_level.upcase)
|
|
10
|
+
remove_old_logfiles(list)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Logger rotates but doesn't delete older files, so we're helping
|
|
14
|
+
# ourselves.
|
|
15
|
+
def remove_old_logfiles(list)
|
|
16
|
+
logfiles_to_keep = list.logfiles_to_keep.to_i
|
|
17
|
+
if logfiles_to_keep < 1
|
|
18
|
+
logfiles_to_keep = list.class.column_defaults['logfiles_to_keep']
|
|
19
|
+
end
|
|
20
|
+
suffix_now = Time.now.strftime("%Y%m%d").to_i
|
|
21
|
+
del_older_than = suffix_now - logfiles_to_keep
|
|
22
|
+
Pathname.glob("#{list.logfile}.????????").each do |file|
|
|
23
|
+
if file.basename.to_s.match(/\.([0-9]{8})$/)
|
|
24
|
+
if del_older_than.to_i >= $1.to_i
|
|
25
|
+
file.unlink
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Schleuder
|
|
2
|
+
def logger
|
|
3
|
+
@logger ||= Logger.new
|
|
4
|
+
end
|
|
5
|
+
module_function :logger
|
|
6
|
+
|
|
7
|
+
class Logger < Syslog::Logger
|
|
8
|
+
include LoggerNotifications
|
|
9
|
+
def initialize
|
|
10
|
+
if RUBY_VERSION.to_f < 2.1
|
|
11
|
+
super('Schleuder')
|
|
12
|
+
else
|
|
13
|
+
super('Schleuder', Syslog::LOG_MAIL)
|
|
14
|
+
end
|
|
15
|
+
# We need some sender-address different from the superadmin-address.
|
|
16
|
+
@from = "#{`whoami`.chomp}@#{`hostname`.chomp}"
|
|
17
|
+
@adminaddresses = Conf.superadmin
|
|
18
|
+
@level = ::Logger.const_get(Conf.log_level.upcase)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Schleuder
|
|
2
|
+
module LoggerNotifications
|
|
3
|
+
def adminaddresses
|
|
4
|
+
@adminaddresses.presence || superadmin
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def superadmin
|
|
8
|
+
Conf.superadmin.presence
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def error(string)
|
|
12
|
+
super(string)
|
|
13
|
+
notify_admin(string)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fatal(string, original_message=nil)
|
|
17
|
+
super(string.to_s + append_original_message(original_message))
|
|
18
|
+
notify_admin(string, original_message)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def notify_admin(thing, original_message=nil, subject='Error')
|
|
22
|
+
# Minimize using other classes here, we don't know what caused the error.
|
|
23
|
+
msg_parts = convert_to_msg_parts(thing, original_message)
|
|
24
|
+
Array(adminaddresses).each do |address, key|
|
|
25
|
+
mail = Mail.new
|
|
26
|
+
mail.from = @from
|
|
27
|
+
mail.to = address
|
|
28
|
+
mail.subject = subject
|
|
29
|
+
mail[:Errors_To] = superadmin
|
|
30
|
+
mail.sender = superadmin
|
|
31
|
+
msg_parts.each do |msg_part|
|
|
32
|
+
mail.add_part(msg_part)
|
|
33
|
+
end
|
|
34
|
+
if @list.present?
|
|
35
|
+
gpg_opts = @list.gpg_sign_options
|
|
36
|
+
if key.present? && key.usable?
|
|
37
|
+
gpg_opts.merge!(encrypt: true, keys: { address => key.fingerprint })
|
|
38
|
+
end
|
|
39
|
+
mail.gpg gpg_opts
|
|
40
|
+
end
|
|
41
|
+
mail.deliver
|
|
42
|
+
end
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def convert_to_msg_parts(thing, original_message)
|
|
49
|
+
msg_parts = Mail::Message.all_to_message_part(thing)
|
|
50
|
+
if original_message.present?
|
|
51
|
+
orig_part = Mail::Part.new
|
|
52
|
+
orig_part.content_type = 'message/rfc822'
|
|
53
|
+
orig_part.content_description = 'The originally incoming message'
|
|
54
|
+
orig_part.body = original_message.to_s
|
|
55
|
+
msg_parts << orig_part
|
|
56
|
+
end
|
|
57
|
+
msg_parts
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def append_original_message(original_message)
|
|
61
|
+
if original_message
|
|
62
|
+
"\n\nOriginal message:\n\n#{original_message.to_s}"
|
|
63
|
+
else
|
|
64
|
+
''
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
module Mail
|
|
2
|
+
# creates a Mail::Message likes schleuder
|
|
3
|
+
def self.create_message_to_list(msg, recipient, list)
|
|
4
|
+
mail = Mail.new(msg)
|
|
5
|
+
mail.list = list
|
|
6
|
+
mail.recipient = recipient
|
|
7
|
+
# don't freeze here, as the mail might not be fully
|
|
8
|
+
# parsed as body is lazy evaluated and might still
|
|
9
|
+
# be changed later.
|
|
10
|
+
mail.original_message = mail.dup #.freeze
|
|
11
|
+
mail
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# TODO: Test if subclassing breaks integration of mail-gpg.
|
|
15
|
+
class Message
|
|
16
|
+
attr_accessor :recipient
|
|
17
|
+
attr_accessor :original_message
|
|
18
|
+
attr_accessor :list
|
|
19
|
+
|
|
20
|
+
# TODO: This should be in initialize(), but I couldn't understand the
|
|
21
|
+
# strange errors about wrong number of arguments when overriding
|
|
22
|
+
# Message#initialize.
|
|
23
|
+
def setup
|
|
24
|
+
if self.encrypted?
|
|
25
|
+
new = self.decrypt(verify: true)
|
|
26
|
+
## Work around a bug in mail-gpg: when decrypting pgp/mime the
|
|
27
|
+
## Date-header is not copied.
|
|
28
|
+
#new.date ||= self.date
|
|
29
|
+
# Test if there's a signed multipart inside the ciphertext
|
|
30
|
+
# ("encapsulated" format of pgp/mime).
|
|
31
|
+
if new.signed?
|
|
32
|
+
new = new.verify
|
|
33
|
+
end
|
|
34
|
+
elsif self.signed?
|
|
35
|
+
new = self.verify
|
|
36
|
+
else
|
|
37
|
+
new = self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
new.list = self.list
|
|
41
|
+
new.recipient = self.recipient
|
|
42
|
+
|
|
43
|
+
new.gpg list.gpg_sign_options
|
|
44
|
+
new.original_message = self.dup.freeze
|
|
45
|
+
# Trigger method early to save the information. Later some information
|
|
46
|
+
# might be gone (e.g. request-keywords that delete subscriptions or
|
|
47
|
+
# keys).
|
|
48
|
+
new.signer
|
|
49
|
+
self.dynamic_pseudoheaders.each do |str|
|
|
50
|
+
new.add_pseudoheader(str)
|
|
51
|
+
end
|
|
52
|
+
new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clean_copy(with_pseudoheaders=false)
|
|
56
|
+
clean = Mail.new
|
|
57
|
+
clean.list = self.list
|
|
58
|
+
clean.gpg self.list.gpg_sign_options
|
|
59
|
+
clean.from = list.email
|
|
60
|
+
clean.subject = self.subject
|
|
61
|
+
|
|
62
|
+
clean.add_msgids(list, self)
|
|
63
|
+
clean.add_list_headers(list)
|
|
64
|
+
clean.add_openpgp_headers(list)
|
|
65
|
+
|
|
66
|
+
if with_pseudoheaders
|
|
67
|
+
new_part = Mail::Part.new
|
|
68
|
+
new_part.body = self.pseudoheaders(list)
|
|
69
|
+
clean.add_part new_part
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Attach body or mime-parts in a new wrapper-part, to preserve the
|
|
73
|
+
# original mime-structure.
|
|
74
|
+
# We can't use self.to_s here — that includes all the headers we *don't*
|
|
75
|
+
# want to copy.
|
|
76
|
+
wrapper_part = Mail::Part.new
|
|
77
|
+
# Copy headers to are relevant for the mime-structure.
|
|
78
|
+
wrapper_part.content_type = self.content_type
|
|
79
|
+
wrapper_part.content_transfer_encoding = self.content_transfer_encoding if self.content_transfer_encoding
|
|
80
|
+
wrapper_part.content_disposition = self.content_disposition if self.content_disposition
|
|
81
|
+
wrapper_part.content_description = self.content_description if self.content_description
|
|
82
|
+
# Copy contents.
|
|
83
|
+
if self.multipart?
|
|
84
|
+
self.parts.each do |part|
|
|
85
|
+
wrapper_part.add_part(part)
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
# We copied the content-headers, so we need to copy the body encoded.
|
|
89
|
+
# Otherwise the content might become unlegible.
|
|
90
|
+
wrapper_part.body = self.body.encoded
|
|
91
|
+
end
|
|
92
|
+
clean.add_part(wrapper_part)
|
|
93
|
+
|
|
94
|
+
clean
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def prepend_part(part)
|
|
98
|
+
self.add_part(part)
|
|
99
|
+
self.parts.unshift(parts.delete_at(parts.size-1))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_public_footer!
|
|
103
|
+
# Add public_footer unless it's empty?.
|
|
104
|
+
add_footer!(:public_footer)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def add_internal_footer!
|
|
108
|
+
add_footer!(:internal_footer)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def was_encrypted?
|
|
112
|
+
Mail::Gpg.encrypted?(original_message)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def signature
|
|
116
|
+
case signatures.size
|
|
117
|
+
when 0
|
|
118
|
+
if multipart?
|
|
119
|
+
signature_multipart_inline
|
|
120
|
+
else
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
when 1
|
|
124
|
+
signatures.first
|
|
125
|
+
else
|
|
126
|
+
raise "Multiple signatures found! Cannot handle!"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def was_validly_signed?
|
|
131
|
+
signature.present? && signature.valid? && signer.present?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def signer
|
|
135
|
+
@signer ||= begin
|
|
136
|
+
if signing_key.present?
|
|
137
|
+
list.subscriptions.where(fingerprint: signing_key.fingerprint).first
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The fingerprint of the signature might be the one of a sub-key, but the
|
|
143
|
+
# subscription-assigned fingerprints are (should be) the ones of the
|
|
144
|
+
# primary keys, so we need to look up the key.
|
|
145
|
+
def signing_key
|
|
146
|
+
if signature.present?
|
|
147
|
+
@signing_key ||= list.keys(signature.fpr).first
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def reply_to_signer(output)
|
|
152
|
+
reply = self.reply
|
|
153
|
+
self.class.all_to_message_part(output).each do |part|
|
|
154
|
+
reply.add_part(part)
|
|
155
|
+
end
|
|
156
|
+
self.signer.send_mail(reply)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def self.all_to_message_part(input)
|
|
160
|
+
Array(input).map do |thing|
|
|
161
|
+
case thing
|
|
162
|
+
when Mail::Part
|
|
163
|
+
thing
|
|
164
|
+
when String, StandardError
|
|
165
|
+
Mail::Part.new do
|
|
166
|
+
body thing.to_s
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
raise "Don't know how to handle input: #{thing.inspect}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def sendkey_request?
|
|
175
|
+
@recipient.match(/-sendkey@/)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def to_owner?
|
|
179
|
+
@recipient.match(/-owner@/)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def request?
|
|
183
|
+
@recipient.match(/-request@/)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def automated_message?
|
|
187
|
+
@recipient.match(/-bounce@/).present? ||
|
|
188
|
+
# Empty Return-Path
|
|
189
|
+
self.return_path.to_s == '<>' ||
|
|
190
|
+
# Auto-Submitted exists and does not equal 'no' and no cron header
|
|
191
|
+
# present, as cron emails have the auto-submitted header.
|
|
192
|
+
( self['Auto-Submitted'].present? && \
|
|
193
|
+
self['Auto-Submitted'].to_s.downcase != 'no' && \
|
|
194
|
+
!self['X-Cron-Env'].present?)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def keywords
|
|
198
|
+
return @keywords if @keywords
|
|
199
|
+
|
|
200
|
+
part = first_plaintext_part
|
|
201
|
+
if part.blank?
|
|
202
|
+
return []
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
@keywords = []
|
|
206
|
+
lines = part.decoded.lines.map do |line|
|
|
207
|
+
# TODO: Find multiline arguments (add-key). Currently add-key has to
|
|
208
|
+
# read the whole body and hope for the best.
|
|
209
|
+
if line.match(/^x-([^:\s]*)[:\s]*(.*)/i)
|
|
210
|
+
command = $1.strip.downcase
|
|
211
|
+
arguments = $2.to_s.strip.downcase.split(/[,; ]{1,}/)
|
|
212
|
+
@keywords << [command, arguments]
|
|
213
|
+
nil
|
|
214
|
+
else
|
|
215
|
+
line
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Work around problems with re-encoding the body. If we delete the
|
|
220
|
+
# content-transfer-encoding prior to re-assigning the body, and let Mail
|
|
221
|
+
# decide itself how to encode, it works. If we don't, some
|
|
222
|
+
# character-sequences are not properly re-encoded.
|
|
223
|
+
part.content_transfer_encoding = nil
|
|
224
|
+
# Make the converted strings (now UTF-8) match what mime-part's headers say,
|
|
225
|
+
# fall back to US-ASCII if none is set.
|
|
226
|
+
# https://tools.ietf.org/html/rfc2046#section-4.1.2
|
|
227
|
+
# -> Default charset is US-ASCII
|
|
228
|
+
part.body = lines.compact.join.encode(part.charset||'US-ASCII')
|
|
229
|
+
|
|
230
|
+
@keywords
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def add_subject_prefix!
|
|
234
|
+
_add_subject_prefix(nil)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def add_subject_prefix_in!
|
|
238
|
+
_add_subject_prefix(:in)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def add_subject_prefix_out!
|
|
242
|
+
_add_subject_prefix(:out)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def add_pseudoheader(string_or_key, value=nil)
|
|
246
|
+
@dynamic_pseudoheaders ||= []
|
|
247
|
+
if value.present?
|
|
248
|
+
@dynamic_pseudoheaders << make_pseudoheader(string_or_key, value)
|
|
249
|
+
else
|
|
250
|
+
@dynamic_pseudoheaders << string_or_key.to_s
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def make_pseudoheader(key, value)
|
|
255
|
+
"#{key.to_s.camelize}: #{value.to_s}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def dynamic_pseudoheaders
|
|
259
|
+
@dynamic_pseudoheaders || []
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def standard_pseudoheaders(list)
|
|
263
|
+
if @standard_pseudoheaders.present?
|
|
264
|
+
return @standard_pseudoheaders
|
|
265
|
+
else
|
|
266
|
+
@standard_pseudoheaders = []
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
Array(list.headers_to_meta).each do |field|
|
|
270
|
+
@standard_pseudoheaders << make_pseudoheader(field.to_s, self.header[field.to_s])
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Careful to add information about the incoming signature. GPGME
|
|
274
|
+
# throws exceptions if it doesn't know the key.
|
|
275
|
+
if self.signature.present?
|
|
276
|
+
# Some versions of gpgme return nil if the key is unknown, so we check
|
|
277
|
+
# for that manually and provide our own fallback. (Calling
|
|
278
|
+
# `signature.key` results in an EOFError in that case.)
|
|
279
|
+
if signing_key.present?
|
|
280
|
+
msg = signature.to_s
|
|
281
|
+
else
|
|
282
|
+
# TODO: I18n
|
|
283
|
+
msg = "Unknown signature by unknown key 0x#{self.signature.fingerprint}"
|
|
284
|
+
end
|
|
285
|
+
else
|
|
286
|
+
# TODO: I18n
|
|
287
|
+
msg = "Unsigned"
|
|
288
|
+
end
|
|
289
|
+
@standard_pseudoheaders << make_pseudoheader(:sig, msg)
|
|
290
|
+
|
|
291
|
+
# TODO: I18n
|
|
292
|
+
@standard_pseudoheaders << make_pseudoheader(
|
|
293
|
+
:enc,
|
|
294
|
+
was_encrypted? ? 'Encrypted' : 'Unencrypted'
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
@standard_pseudoheaders
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def pseudoheaders(list)
|
|
301
|
+
(standard_pseudoheaders(list) + dynamic_pseudoheaders).flatten.join("\n") + "\n"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def add_msgids(list, orig)
|
|
305
|
+
if list.keep_msgid
|
|
306
|
+
# Don't use `orig['in-reply-to']` here, because that sometimes fails to
|
|
307
|
+
# parse the original value and then returns it without the
|
|
308
|
+
# angle-brackets.
|
|
309
|
+
self.message_id = clutch_anglebrackets(orig.message_id)
|
|
310
|
+
self.in_reply_to = clutch_anglebrackets(orig.in_reply_to)
|
|
311
|
+
self.references = clutch_anglebrackets(orig.references)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def add_list_headers(list)
|
|
316
|
+
if list.include_list_headers
|
|
317
|
+
self['List-Id'] = "<#{list.email.gsub('@', '.')}>"
|
|
318
|
+
self['List-Owner'] = "<mailto:#{list.owner_address}> (Use list's public key)"
|
|
319
|
+
self['List-Help'] = '<https://schleuder.nadir.org/>'
|
|
320
|
+
|
|
321
|
+
postmsg = if list.receive_admin_only
|
|
322
|
+
"NO (Admins only)"
|
|
323
|
+
elsif list.receive_authenticated_only
|
|
324
|
+
"<mailto:#{list.email}> (Subscribers only)"
|
|
325
|
+
else
|
|
326
|
+
"<mailto:#{list.email}>"
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
self['List-Post'] = postmsg
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def add_openpgp_headers(list)
|
|
334
|
+
if list.include_openpgp_header
|
|
335
|
+
|
|
336
|
+
if list.openpgp_header_preference == 'none'
|
|
337
|
+
pref = ''
|
|
338
|
+
else
|
|
339
|
+
pref = "preference=#{list.openpgp_header_preference}"
|
|
340
|
+
|
|
341
|
+
# TODO: simplify.
|
|
342
|
+
pref << ' ('
|
|
343
|
+
if list.receive_admin_only
|
|
344
|
+
pref << 'Only encrypted and signed emails by list-admins are accepted'
|
|
345
|
+
elsif ! list.receive_authenticated_only
|
|
346
|
+
if list.receive_encrypted_only && list.receive_signed_only
|
|
347
|
+
pref << 'Only encrypted and signed emails are accepted'
|
|
348
|
+
elsif list.receive_encrypted_only && ! list.receive_signed_only
|
|
349
|
+
pref << 'Only encrypted emails are accepted'
|
|
350
|
+
elsif ! list.receive_encrypted_only && list.receive_signed_only
|
|
351
|
+
pref << 'Only signed emails are accepted'
|
|
352
|
+
else
|
|
353
|
+
pref << 'All kind of emails are accepted'
|
|
354
|
+
end
|
|
355
|
+
elsif list.receive_authenticated_only
|
|
356
|
+
if list.receive_encrypted_only
|
|
357
|
+
pref << 'Only encrypted and signed emails by subscribers are accepted'
|
|
358
|
+
else
|
|
359
|
+
pref << 'Only signed emails by subscribers are accepted'
|
|
360
|
+
end
|
|
361
|
+
else
|
|
362
|
+
pref << 'All kind of emails are accepted'
|
|
363
|
+
end
|
|
364
|
+
pref << ')'
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
fingerprint = list.fingerprint
|
|
368
|
+
comment = "(Send an email to #{list.sendkey_address} to receive the public-key)"
|
|
369
|
+
|
|
370
|
+
self['OpenPGP'] = "id=0x#{fingerprint} #{comment}; #{pref}"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def empty?
|
|
375
|
+
if self.multipart?
|
|
376
|
+
if self.parts.empty?
|
|
377
|
+
return true
|
|
378
|
+
else
|
|
379
|
+
# Test parts recursively. E.g. Thunderbird with activated
|
|
380
|
+
# memoryhole-headers send nested parts that might still be empty.
|
|
381
|
+
return parts.inject(true) { |result, part| result && part.empty? }
|
|
382
|
+
end
|
|
383
|
+
else
|
|
384
|
+
return self.body.empty?
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def first_plaintext_part(part=nil)
|
|
389
|
+
part ||= self
|
|
390
|
+
if part.multipart?
|
|
391
|
+
first_plaintext_part(part.parts.first)
|
|
392
|
+
elsif part.mime_type == 'text/plain'
|
|
393
|
+
part
|
|
394
|
+
else
|
|
395
|
+
nil
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def attach_list_key!(list)
|
|
401
|
+
filename = "#{list.email}.asc"
|
|
402
|
+
self.add_file({
|
|
403
|
+
filename: filename,
|
|
404
|
+
content: list.export_key
|
|
405
|
+
})
|
|
406
|
+
self.attachments[filename].content_type = 'application/pgp-keys'
|
|
407
|
+
self.attachments[filename].content_description = 'OpenPGP public key'
|
|
408
|
+
true
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
private
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def add_footer!(footer_attribute)
|
|
415
|
+
if self.list.blank? || self.list.send(footer_attribute).to_s.empty?
|
|
416
|
+
return
|
|
417
|
+
end
|
|
418
|
+
footer_part = Mail::Part.new
|
|
419
|
+
footer_part.body = self.list.send(footer_attribute).to_s
|
|
420
|
+
if wrapped_single_text_part?
|
|
421
|
+
self.parts.first.add_part footer_part
|
|
422
|
+
else
|
|
423
|
+
self.add_part footer_part
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def wrapped_single_text_part?
|
|
428
|
+
parts.size == 1 &&
|
|
429
|
+
parts.first.mime_type == 'multipart/mixed' &&
|
|
430
|
+
parts.first.parts.size == 1 &&
|
|
431
|
+
parts.first.parts.first.mime_type == 'text/plain'
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def _add_subject_prefix(suffix)
|
|
435
|
+
attrib = "subject_prefix"
|
|
436
|
+
if suffix
|
|
437
|
+
attrib << "_#{suffix}"
|
|
438
|
+
end
|
|
439
|
+
if ! self.list.respond_to?(attrib)
|
|
440
|
+
return false
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
string = self.list.send(attrib).to_s.strip
|
|
444
|
+
if ! string.empty?
|
|
445
|
+
prefix = "#{string} "
|
|
446
|
+
# Only insert prefix if it's not present already.
|
|
447
|
+
if self.subject.nil?
|
|
448
|
+
self.subject = string
|
|
449
|
+
elsif ! self.subject.include?(prefix)
|
|
450
|
+
self.subject = "#{prefix}#{self.subject}"
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Looking for signatures in each part. They are not aggregated into the main part.
|
|
456
|
+
# We only return the signature if all parts are validly signed by the same key.
|
|
457
|
+
def signature_multipart_inline
|
|
458
|
+
fingerprints = parts.map do |part|
|
|
459
|
+
if part.signature_valid?
|
|
460
|
+
part.signature.fpr
|
|
461
|
+
else
|
|
462
|
+
nil
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
if fingerprints.uniq.size == 1
|
|
466
|
+
parts.first.signature
|
|
467
|
+
else
|
|
468
|
+
nil
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def clutch_anglebrackets(input)
|
|
473
|
+
Array(input).map do |string|
|
|
474
|
+
if string.first == '<'
|
|
475
|
+
string
|
|
476
|
+
else
|
|
477
|
+
"<#{string}>"
|
|
478
|
+
end
|
|
479
|
+
end.join(' ')
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|