schleuder 4.0.2 → 5.0.0
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 +4 -4
- data/README.md +6 -9
- data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +1 -1
- data/db/migrate/20211106112020_change_boolean_values_to_integers.rb +46 -0
- data/db/migrate/20211107151309_add_limits_to_string_columns.rb +28 -0
- data/db/migrate/20220910170110_add_key_auto_import_from_email.rb +11 -0
- data/db/schema.rb +16 -16
- data/etc/list-defaults.yml +16 -0
- data/etc/schleuder.yml +29 -11
- data/lib/schleuder/cli.rb +15 -2
- data/lib/schleuder/conf.rb +23 -3
- data/lib/schleuder/email_key_importer.rb +91 -0
- data/lib/schleuder/filters/post_decryption/35_key_auto_import_from_attachments.rb +21 -0
- data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +1 -1
- data/lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb +36 -4
- data/lib/schleuder/filters/pre_decryption/60_key_auto_import_from_autocrypt_header.rb +9 -0
- data/lib/schleuder/filters_runner.rb +1 -30
- data/lib/schleuder/gpgme/ctx.rb +34 -93
- data/lib/schleuder/gpgme/key.rb +1 -1
- data/lib/schleuder/gpgme/key_extractor.rb +30 -0
- data/lib/schleuder/http.rb +56 -0
- data/lib/schleuder/key_fetcher.rb +89 -0
- data/lib/schleuder/keyword_handlers/key_management.rb +2 -2
- data/lib/schleuder/keyword_handlers/subscription_management.rb +19 -3
- data/lib/schleuder/list.rb +26 -10
- data/lib/schleuder/list_builder.rb +1 -1
- data/lib/schleuder/logger.rb +1 -1
- data/lib/schleuder/mail/gpg/decrypted_part.rb +20 -0
- data/lib/schleuder/mail/gpg/delivery_handler.rb +38 -0
- data/lib/schleuder/mail/gpg/encrypted_part.rb +29 -5
- data/lib/schleuder/mail/gpg/gpgme_ext.rb +8 -0
- data/lib/schleuder/mail/gpg/gpgme_helper.rb +155 -0
- data/lib/schleuder/mail/gpg/inline_decrypted_message.rb +82 -0
- data/lib/schleuder/mail/gpg/inline_signed_message.rb +73 -0
- data/lib/schleuder/mail/gpg/mime_signed_message.rb +28 -0
- data/lib/schleuder/mail/gpg/missing_keys_error.rb +6 -0
- data/lib/schleuder/mail/gpg/sign_part.rb +19 -9
- data/lib/schleuder/mail/gpg/signed_part.rb +37 -0
- data/lib/schleuder/mail/gpg/verified_part.rb +10 -0
- data/lib/schleuder/mail/gpg/verify_result_attribute.rb +32 -0
- data/lib/schleuder/mail/gpg/version_part.rb +22 -0
- data/lib/schleuder/mail/gpg.rb +236 -7
- data/lib/schleuder/mail/message.rb +98 -14
- data/lib/schleuder/runner.rb +40 -10
- data/lib/schleuder/sks_client.rb +18 -0
- data/lib/schleuder/version.rb +1 -1
- data/lib/schleuder/vks_client.rb +24 -0
- data/lib/schleuder-api-daemon/routes/key.rb +22 -1
- data/lib/schleuder.rb +11 -7
- data/locales/de.yml +38 -19
- data/locales/en.yml +22 -3
- metadata +58 -21
data/lib/schleuder/gpgme/ctx.rb
CHANGED
@@ -7,6 +7,9 @@ module GPGME
|
|
7
7
|
'new_subkeys' => 8
|
8
8
|
}
|
9
9
|
|
10
|
+
# This differs from import_filtered() in that it doesn't filter the keys at
|
11
|
+
# all, and that it returns the import-results themselves, not strings based
|
12
|
+
# on those results.
|
10
13
|
def keyimport(keydata)
|
11
14
|
self.import_keys(GPGME::Data.new(keydata))
|
12
15
|
result = self.import_result
|
@@ -33,13 +36,11 @@ module GPGME
|
|
33
36
|
end
|
34
37
|
|
35
38
|
def find_keys(input=nil, secret_only=nil)
|
36
|
-
|
37
|
-
keys(input, secret_only)
|
39
|
+
keys(normalize_key_identifier(input), secret_only)
|
38
40
|
end
|
39
41
|
|
40
42
|
def find_distinct_key(input=nil, secret_only=nil)
|
41
|
-
|
42
|
-
keys = keys(input, secret_only)
|
43
|
+
keys = keys(normalize_key_identifier(input), secret_only)
|
43
44
|
if keys.size == 1
|
44
45
|
keys.first
|
45
46
|
else
|
@@ -47,16 +48,16 @@ module GPGME
|
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
|
-
def
|
51
|
+
def normalize_key_identifier(input)
|
51
52
|
case input
|
52
53
|
when /.*?([^ <>]+@[^ <>]+).*?/
|
53
|
-
|
54
|
+
"<#{$1}>"
|
54
55
|
when /^http/
|
55
|
-
|
56
|
+
input
|
56
57
|
when Conf::FINGERPRINT_REGEXP
|
57
|
-
|
58
|
+
"0x#{input.gsub(/^0x/, '')}"
|
58
59
|
else
|
59
|
-
|
60
|
+
input
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
@@ -88,85 +89,23 @@ module GPGME
|
|
88
89
|
GPGME::Engine.info.find {|e| e.protocol == GPGME::PROTOCOL_OpenPGP }
|
89
90
|
end
|
90
91
|
|
91
|
-
def
|
92
|
-
#
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
output.compact.join("\n")
|
100
|
-
end
|
101
|
-
|
102
|
-
def refresh_key(fingerprint)
|
103
|
-
args = "#{keyserver_arg} #{import_filter_arg} --refresh-keys #{fingerprint}"
|
104
|
-
gpgerr, gpgout, exitcode = self.class.gpgcli(args)
|
105
|
-
|
106
|
-
if exitcode > 0
|
107
|
-
# Return filtered error messages. Include gpgkeys-messages from stdout
|
108
|
-
# (gpg 2.0 does that), which could e.g. report a failure to connect to
|
109
|
-
# the keyserver.
|
110
|
-
# TODO: Revisit this once we don't do network access via GPG
|
111
|
-
# anymore.
|
112
|
-
res = [
|
113
|
-
refresh_key_filter_messages(gpgerr),
|
114
|
-
refresh_key_filter_messages(gpgout).grep(/^gpgkeys: /)
|
115
|
-
].flatten.compact
|
116
|
-
# if there was an error that we don't filter out,
|
117
|
-
# we better kill dirmngr, so it hopefully won't suffer
|
118
|
-
# from the same error during the next run.
|
119
|
-
# See #309 for background
|
120
|
-
if !res.empty?
|
121
|
-
`gpgconf --kill dirmngr`
|
122
|
-
end
|
123
|
-
res.join("\n")
|
124
|
-
else
|
125
|
-
lines = translate_output('key_updated', gpgout).reject do |line|
|
126
|
-
# Reduce the noise a little.
|
127
|
-
line.match(/.* \(unchanged\):$/)
|
92
|
+
def import_filtered(input, gpg_extra_arg='')
|
93
|
+
# Import through gpgcli so we can use import-filter. GPGME still does
|
94
|
+
# not provide that feature (as of summer 2023): <https://dev.gnupg.org/T4721> :(
|
95
|
+
gpgerr, gpgout, exitcode = self.class.gpgcli("#{import_filter_arg} #{gpg_extra_arg} --import") do |stdin, stdout, stderr|
|
96
|
+
# Wrap this into a block because gpg breaks the pipe if it encounters invalid data.
|
97
|
+
begin
|
98
|
+
stdin.print input
|
99
|
+
rescue Errno::EPIPE
|
128
100
|
end
|
129
|
-
|
101
|
+
stdin.close
|
102
|
+
stdout.readlines
|
130
103
|
end
|
131
|
-
|
132
|
-
|
133
|
-
def fetch_key(input)
|
134
|
-
arguments, error = fetch_key_gpg_arguments_for(input)
|
135
|
-
return error if error
|
136
|
-
|
137
|
-
gpgerr, gpgout, exitcode = self.class.gpgcli("#{import_filter_arg} #{arguments}")
|
138
|
-
|
139
|
-
# Unfortunately gpg doesn't exit with code > 0 if `--fetch-key` fails.
|
140
|
-
if exitcode > 0 || gpgerr.grep(/ unable to fetch /).presence
|
141
|
-
"Fetching #{input} did not succeed:\n#{gpgerr.join("\n")}"
|
142
|
-
else
|
143
|
-
translate_output('key_fetched', gpgout).join("\n")
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
def fetch_key_gpg_arguments_for(input)
|
148
|
-
case input
|
149
|
-
when Conf::FINGERPRINT_REGEXP
|
150
|
-
"#{keyserver_arg} --recv-key #{input}"
|
151
|
-
when /^http/
|
152
|
-
"--fetch-key #{input}"
|
153
|
-
when /@/
|
154
|
-
# --recv-key doesn't work with email-addresses, so we use --locate-key
|
155
|
-
# restricted to keyservers.
|
156
|
-
"#{keyserver_arg} --auto-key-locate keyserver --locate-key #{input}"
|
104
|
+
if exitcode > 0
|
105
|
+
RuntimeError.new(gpgerr.join("\n"))
|
157
106
|
else
|
158
|
-
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def translate_output(locale_key, gpgoutput)
|
163
|
-
import_states = translate_import_data(gpgoutput)
|
164
|
-
strings = import_states.map do |fingerprint, states|
|
165
|
-
key = find_distinct_key(fingerprint)
|
166
|
-
I18n.t(locale_key, key_summary: key.summary,
|
167
|
-
states: states.to_sentence)
|
107
|
+
translate_import_data(gpgout)
|
168
108
|
end
|
169
|
-
strings
|
170
109
|
end
|
171
110
|
|
172
111
|
def translate_import_data(gpgoutput)
|
@@ -210,7 +149,7 @@ module GPGME
|
|
210
149
|
errors = []
|
211
150
|
output = []
|
212
151
|
base_cmd = gpg_engine.file_name
|
213
|
-
base_args = '--no-greeting --
|
152
|
+
base_args = '--no-greeting --quiet --armor --trust-model always --no-tty --command-fd 0 --status-fd 1'
|
214
153
|
cmd = [base_cmd, base_args, args].flatten.join(' ')
|
215
154
|
Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
|
216
155
|
if block_given?
|
@@ -223,19 +162,21 @@ module GPGME
|
|
223
162
|
exitcode = thread.value.exitstatus
|
224
163
|
end
|
225
164
|
|
165
|
+
# Don't treat warnings as errors but log them.
|
166
|
+
errors = errors.map do |line|
|
167
|
+
if line.match?(/gpg: WARNING: (unsafe permissions on homedir|using insecure memory)/i)
|
168
|
+
Schleuder.logger.warn(line)
|
169
|
+
nil
|
170
|
+
else
|
171
|
+
line
|
172
|
+
end
|
173
|
+
end.compact
|
174
|
+
|
226
175
|
[errors, output, exitcode]
|
227
176
|
rescue Errno::ENOENT
|
228
177
|
raise 'Need gpg in $PATH or in $GPGBIN'
|
229
178
|
end
|
230
179
|
|
231
|
-
def keyserver_arg
|
232
|
-
if Conf.keyserver.present?
|
233
|
-
"--keyserver #{Conf.keyserver}"
|
234
|
-
else
|
235
|
-
''
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
180
|
def import_filter_arg
|
240
181
|
%{ --import-filter drop-sig='sig_created_d > 0000-00-00'}
|
241
182
|
end
|
data/lib/schleuder/gpgme/key.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
module GPGME
|
2
|
+
class KeyExtractor
|
3
|
+
# This takes key material and returns those keys from it, that have a UID
|
4
|
+
# matching the given email address, stripped by all other UIDs.
|
5
|
+
def self.extract_by_email_address(email_address, keydata)
|
6
|
+
orig_gnupghome = ENV['GNUPGHOME']
|
7
|
+
ENV['GNUPGHOME'] = Dir.mktmpdir
|
8
|
+
gpg = GPGME::Ctx.new(armor: true)
|
9
|
+
gpg_arg = %{ --import-filter keep-uid='mbox = #{email_address}'}
|
10
|
+
gpg.import_filtered(keydata, gpg_arg)
|
11
|
+
# Return the fingerprint and the exported, filtered keydata, because
|
12
|
+
# passing the key objects around led to strange problems with some keys,
|
13
|
+
# which produced only a blank string as return value of export().
|
14
|
+
result = {}
|
15
|
+
gpg.keys.each do |tmp_key|
|
16
|
+
# Skip this key if it has
|
17
|
+
# * no UID – because none survived the import-filter,
|
18
|
+
# * more than one UID – which means the import-filtering failed or
|
19
|
+
# something else went wrong during import.
|
20
|
+
if tmp_key.uids.size == 1
|
21
|
+
result[tmp_key.fingerprint] = tmp_key.armored
|
22
|
+
end
|
23
|
+
end
|
24
|
+
result
|
25
|
+
ensure
|
26
|
+
FileUtils.remove_entry(ENV['GNUPGHOME'])
|
27
|
+
ENV['GNUPGHOME'] = orig_gnupghome
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class NetworkError < StandardError; end
|
3
|
+
|
4
|
+
class NotFoundError < StandardError; end
|
5
|
+
|
6
|
+
class Http
|
7
|
+
attr_reader :request, :response
|
8
|
+
|
9
|
+
def initialize(url, options={})
|
10
|
+
@request = Typhoeus::Request.new(url, default_options.merge(options))
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
@response = @request.run
|
15
|
+
if @response.success?
|
16
|
+
@response.body
|
17
|
+
elsif @response.timed_out?
|
18
|
+
raise_network_error(response, 'HTTP Request timed out.')
|
19
|
+
elsif @response.code == 404
|
20
|
+
NotFoundError.new
|
21
|
+
elsif @response.code == 0
|
22
|
+
# This happens e.g. if no response could be received.
|
23
|
+
raise_network_error(@response, 'No HTTP response received.')
|
24
|
+
else
|
25
|
+
RuntimeError.new(@response.body.to_s.presence || @response.return_message)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.get(url)
|
30
|
+
nth_attempt ||= 1
|
31
|
+
new(url).run
|
32
|
+
rescue NetworkError => error
|
33
|
+
nth_attempt += 1
|
34
|
+
if nth_attempt < 4
|
35
|
+
retry
|
36
|
+
else
|
37
|
+
return error
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def raise_network_error(response, fallback_msg)
|
44
|
+
raise NetworkError.new(
|
45
|
+
response.body.to_s.presence || response.return_message || fallback_msg
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def default_options
|
50
|
+
{
|
51
|
+
followlocation: true,
|
52
|
+
proxy: Conf.http_proxy.presence
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class KeyFetcher
|
3
|
+
def initialize(list)
|
4
|
+
@list = list
|
5
|
+
end
|
6
|
+
|
7
|
+
def fetch(input, locale_key='key_fetched')
|
8
|
+
result = case input
|
9
|
+
when /^http/
|
10
|
+
fetch_key_by_url(input)
|
11
|
+
when Conf::EMAIL_REGEXP
|
12
|
+
fetch_key_from_keyserver('email', input)
|
13
|
+
when Conf::FINGERPRINT_REGEXP
|
14
|
+
fetch_key_from_keyserver('fingerprint', input)
|
15
|
+
else
|
16
|
+
return I18n.t('key_fetcher.invalid_input')
|
17
|
+
end
|
18
|
+
interpret_fetch_result(result, locale_key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch_key_by_url(url)
|
22
|
+
case result = Schleuder::Http.get(url)
|
23
|
+
when NotFoundError
|
24
|
+
NotFoundError.new(I18n.t('key_fetcher.url_not_found', url: url))
|
25
|
+
else
|
26
|
+
result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch_key_from_keyserver(type, input)
|
31
|
+
if Conf.vks_keyserver.present?
|
32
|
+
result = Schleuder::VksClient.get(type, input)
|
33
|
+
end
|
34
|
+
if (result.blank? || ! result.is_a?(String)) && Conf.sks_keyserver.present?
|
35
|
+
result = Schleuder::SksClient.get(input)
|
36
|
+
end
|
37
|
+
|
38
|
+
case result
|
39
|
+
when nil
|
40
|
+
RuntimeError.new('No keyserver configured, cannot query anything')
|
41
|
+
when NotFoundError
|
42
|
+
NotFoundError.new(I18n.t('key_fetcher.not_found', input: input))
|
43
|
+
else
|
44
|
+
result
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def interpret_fetch_result(result, locale_key)
|
51
|
+
case result
|
52
|
+
when ''
|
53
|
+
I18n.t('key_fetcher.general_error', error: 'Empty response from server')
|
54
|
+
when String
|
55
|
+
import(result, locale_key)
|
56
|
+
when NotFoundError
|
57
|
+
result.to_s
|
58
|
+
when StandardError
|
59
|
+
I18n.t('key_fetcher.general_error', error: result)
|
60
|
+
else
|
61
|
+
raise_unexpected_error(result)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def import(input, locale_key)
|
66
|
+
result = @list.gpg.import_filtered(input)
|
67
|
+
case result
|
68
|
+
when StandardError
|
69
|
+
I18n.t('key_fetcher.import_error', error: result)
|
70
|
+
when Hash
|
71
|
+
translate_output(locale_key, result).join("\n")
|
72
|
+
else
|
73
|
+
raise_unexpected_error(result)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def translate_output(locale_key, import_states)
|
78
|
+
import_states.map do |fingerprint, states|
|
79
|
+
key = @list.gpg.find_distinct_key(fingerprint)
|
80
|
+
I18n.t(locale_key, key_summary: key.summary,
|
81
|
+
states: states.to_sentence)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def raise_unexpected_error(thing)
|
86
|
+
raise "Unexpected output => #{thing.inspect}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -15,7 +15,7 @@ module Schleuder
|
|
15
15
|
import_key_from_body
|
16
16
|
else
|
17
17
|
@list.logger.debug 'Found no attachments and an empty body - sending error message'
|
18
|
-
I18n.t('keyword_handlers.key_management.no_content_found')
|
18
|
+
return I18n.t('keyword_handlers.key_management.no_content_found')
|
19
19
|
end
|
20
20
|
|
21
21
|
import_stati = results.compact.collect(&:imports).flatten
|
@@ -125,7 +125,7 @@ module Schleuder
|
|
125
125
|
|
126
126
|
def import_keys_from_attachments
|
127
127
|
@mail.attachments.map do |attachment|
|
128
|
-
import_from_string(attachment.body.
|
128
|
+
import_from_string(attachment.body.decoded)
|
129
129
|
end
|
130
130
|
end
|
131
131
|
|
@@ -22,9 +22,25 @@ module Schleuder
|
|
22
22
|
while @arguments.first.present? && @arguments.first.match(/^(0x)?[a-f0-9]+$/i)
|
23
23
|
fingerprint << @arguments.shift.downcase
|
24
24
|
end
|
25
|
-
#
|
26
|
-
|
27
|
-
|
25
|
+
# If the collected values aren't a valid fingerprint, then the input
|
26
|
+
# didn't conform with what this code expects, and then the other
|
27
|
+
# values shouldn't be used.
|
28
|
+
unless GPGME::Key.valid_fingerprint?(fingerprint)
|
29
|
+
return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
|
30
|
+
end
|
31
|
+
if @arguments.present?
|
32
|
+
# Use possibly remaining args as flags.
|
33
|
+
adminflag = @arguments.shift.to_s.downcase.presence
|
34
|
+
unless ['true', 'false'].include?(adminflag)
|
35
|
+
return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
|
36
|
+
end
|
37
|
+
if @arguments.present?
|
38
|
+
deliveryflag = @arguments.shift.to_s.downcase.presence
|
39
|
+
unless ['true', 'false'].include?(deliveryflag)
|
40
|
+
return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
28
44
|
end
|
29
45
|
|
30
46
|
sub, _ = @list.subscribe(email, fingerprint, adminflag, deliveryflag)
|
data/lib/schleuder/list.rb
CHANGED
@@ -4,10 +4,10 @@ module Schleuder
|
|
4
4
|
has_many :subscriptions, dependent: :destroy
|
5
5
|
before_destroy :delete_listdirs
|
6
6
|
|
7
|
-
serialize :headers_to_meta, JSON
|
8
|
-
serialize :bounces_drop_on_headers, JSON
|
9
|
-
serialize :keywords_admin_only, JSON
|
10
|
-
serialize :keywords_admin_notify, JSON
|
7
|
+
serialize :headers_to_meta, coder: JSON
|
8
|
+
serialize :bounces_drop_on_headers, coder: JSON
|
9
|
+
serialize :keywords_admin_only, coder: JSON
|
10
|
+
serialize :keywords_admin_notify, coder: JSON
|
11
11
|
|
12
12
|
validates :email, presence: true, uniqueness: true, email: true
|
13
13
|
validates :fingerprint, presence: true, fingerprint: true
|
@@ -23,7 +23,8 @@ module Schleuder
|
|
23
23
|
:bounces_notify_admins,
|
24
24
|
:include_list_headers,
|
25
25
|
:include_openpgp_header,
|
26
|
-
:forward_all_incoming_to_admins,
|
26
|
+
:forward_all_incoming_to_admins,
|
27
|
+
:key_auto_import_from_email, boolean: true
|
27
28
|
validates_each :headers_to_meta,
|
28
29
|
:keywords_admin_only,
|
29
30
|
:keywords_admin_notify do |record, attrib, value|
|
@@ -197,11 +198,26 @@ module Schleuder
|
|
197
198
|
end
|
198
199
|
|
199
200
|
def refresh_keys
|
200
|
-
|
201
|
+
# reorder keys so the update pattern is random
|
202
|
+
output = self.keys.shuffle.map do |key|
|
203
|
+
# Sleep a short while to make traffic analysis less easy.
|
204
|
+
sleep rand(1.0..5.0)
|
205
|
+
key_fetcher.fetch(key.fingerprint, 'key_updated').presence
|
206
|
+
end
|
207
|
+
# Filter out some "noise" (if a key was unchanged, it wasn't really updated, was it?)
|
208
|
+
# It would be nice to prevent these "false" lines in the first place, but I don't know how.
|
209
|
+
output.reject! do |line|
|
210
|
+
line.match('updated \(unchanged\)')
|
211
|
+
end
|
212
|
+
output.compact.join("\n")
|
201
213
|
end
|
202
214
|
|
203
215
|
def fetch_keys(input)
|
204
|
-
|
216
|
+
key_fetcher.fetch(input)
|
217
|
+
end
|
218
|
+
|
219
|
+
def key_fetcher
|
220
|
+
@key_fetcher ||= KeyFetcher.new(self)
|
205
221
|
end
|
206
222
|
|
207
223
|
def self.by_recipient(recipient)
|
@@ -350,7 +366,7 @@ module Schleuder
|
|
350
366
|
next
|
351
367
|
end
|
352
368
|
|
353
|
-
if ! self.deliver_selfsent && incoming_mail
|
369
|
+
if ! self.deliver_selfsent && incoming_mail&.was_validly_signed? && ( subscription == incoming_mail&.signer )
|
354
370
|
logger.info "Not sending to #{subscription.email}: delivery of self sent is disabled."
|
355
371
|
next
|
356
372
|
end
|
@@ -373,14 +389,14 @@ module Schleuder
|
|
373
389
|
end
|
374
390
|
|
375
391
|
def delete_listdirs
|
376
|
-
if File.
|
392
|
+
if File.exist?(self.listdir)
|
377
393
|
FileUtils.rm_rf(self.listdir, secure: true)
|
378
394
|
Schleuder.logger.info "Deleted #{self.listdir}"
|
379
395
|
end
|
380
396
|
# If listlogs_dir is different from lists_dir, the logfile still exists
|
381
397
|
# and needs to be deleted, too.
|
382
398
|
logfile_dir = File.dirname(self.logfile)
|
383
|
-
if File.
|
399
|
+
if File.exist?(logfile_dir)
|
384
400
|
FileUtils.rm_rf(logfile_dir, secure: true)
|
385
401
|
Schleuder.logger.info "Deleted #{logfile_dir}"
|
386
402
|
end
|
data/lib/schleuder/logger.rb
CHANGED
@@ -9,7 +9,7 @@ module Schleuder
|
|
9
9
|
def initialize
|
10
10
|
super('Schleuder', Syslog::LOG_MAIL)
|
11
11
|
# We need some sender-address different from the superadmin-address.
|
12
|
-
@from = "#{Etc.getlogin}@#{Socket.gethostname}"
|
12
|
+
@from = "#{Etc.getlogin}@#{Addrinfo.getaddrinfo(Socket.gethostname, nil).first.getnameinfo.first}"
|
13
13
|
@adminaddresses = Conf.superadmin
|
14
14
|
@level = ::Logger.const_get(Conf.log_level.upcase)
|
15
15
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'schleuder/mail/gpg/verified_part'
|
2
|
+
module Mail
|
3
|
+
module Gpg
|
4
|
+
class DecryptedPart < VerifiedPart
|
5
|
+
|
6
|
+
# options are:
|
7
|
+
#
|
8
|
+
# :verify: decrypt and verify
|
9
|
+
def initialize(cipher_part, options = {})
|
10
|
+
if cipher_part.mime_type != EncryptedPart::CONTENT_TYPE
|
11
|
+
raise EncodingError.new("RFC 3156 incorrect mime type for encrypted part '#{cipher_part.mime_type}'")
|
12
|
+
end
|
13
|
+
|
14
|
+
decrypted = GpgmeHelper.decrypt(cipher_part.body.decoded, options)
|
15
|
+
self.verify_result = decrypted.verify_result if options[:verify]
|
16
|
+
super(decrypted)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Mail
|
2
|
+
module Gpg
|
3
|
+
class DeliveryHandler
|
4
|
+
|
5
|
+
def self.deliver_mail(mail)
|
6
|
+
if mail.gpg
|
7
|
+
encrypted_mail = nil
|
8
|
+
begin
|
9
|
+
options = mail.gpg.is_a?(TrueClass) ? { encrypt: true } : mail.gpg
|
10
|
+
if options[:encrypt]
|
11
|
+
encrypted_mail = Mail::Gpg.encrypt(mail, options)
|
12
|
+
elsif options[:sign] || options[:sign_as]
|
13
|
+
encrypted_mail = Mail::Gpg.sign(mail, options)
|
14
|
+
else
|
15
|
+
# encrypt and sign are off -> do not encrypt or sign
|
16
|
+
yield
|
17
|
+
end
|
18
|
+
rescue StandardError
|
19
|
+
raise $! if mail.raise_encryption_errors
|
20
|
+
end
|
21
|
+
if encrypted_mail
|
22
|
+
if dm = mail.delivery_method
|
23
|
+
encrypted_mail.instance_variable_set :@delivery_method, dm
|
24
|
+
end
|
25
|
+
encrypted_mail.perform_deliveries = mail.perform_deliveries
|
26
|
+
encrypted_mail.raise_delivery_errors = mail.raise_delivery_errors
|
27
|
+
encrypted_mail.deliver
|
28
|
+
end
|
29
|
+
else
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
rescue StandardError
|
33
|
+
raise $! if mail.raise_delivery_errors
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -1,13 +1,37 @@
|
|
1
|
-
module Mail
|
2
|
-
module Gpg
|
3
|
-
class EncryptedPart < Mail::Part
|
4
|
-
alias_method :initialize_mailgpg, :initialize
|
1
|
+
module Mail
|
2
|
+
module Gpg
|
3
|
+
class EncryptedPart < Mail::Part
|
5
4
|
|
5
|
+
CONTENT_TYPE = 'application/octet-stream'
|
6
|
+
|
7
|
+
# options are:
|
8
|
+
#
|
9
|
+
# :signers : sign using this key (give the corresponding email address)
|
10
|
+
# :password: passphrase for the signing key
|
11
|
+
# :recipients : array of receiver addresses
|
12
|
+
# :keys : A hash mapping recipient email addresses to public keys or public
|
13
|
+
# key ids. Imports any keys given here that are not already part of the
|
14
|
+
# local keychain before sending the mail. If this option is given, strictly
|
15
|
+
# only the key material from this hash is used, ignoring any keys for
|
16
|
+
# recipients that might have been added to the local key chain but are
|
17
|
+
# not mentioned here.
|
18
|
+
# :always_trust : send encrypted mail to untrusted receivers, true by default
|
19
|
+
# :filename : define a custom name for the encrypted file attachment
|
6
20
|
def initialize(cleartext_mail, options = {})
|
7
21
|
if cleartext_mail.protected_headers_subject
|
8
22
|
cleartext_mail.content_type_parameters['protected-headers'] = 'v1'
|
9
23
|
end
|
10
|
-
|
24
|
+
|
25
|
+
options = { always_trust: true }.merge options
|
26
|
+
|
27
|
+
encrypted = GpgmeHelper.encrypt(cleartext_mail.encoded, options)
|
28
|
+
super() do
|
29
|
+
body encrypted.to_s
|
30
|
+
filename = options[:filename] || 'encrypted.asc'
|
31
|
+
content_type "#{CONTENT_TYPE}; name=\"#{filename}\""
|
32
|
+
content_disposition "inline; filename=\"#{filename}\""
|
33
|
+
content_description 'OpenPGP encrypted message'
|
34
|
+
end
|
11
35
|
end
|
12
36
|
end
|
13
37
|
end
|