schleuder 2.2.4 → 3.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|