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