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/mail/gpg.rb
CHANGED
@@ -1,15 +1,244 @@
|
|
1
|
+
require 'schleuder/mail/gpg/missing_keys_error'
|
2
|
+
require 'schleuder/mail/gpg/version_part'
|
3
|
+
require 'schleuder/mail/gpg/decrypted_part'
|
4
|
+
require 'schleuder/mail/gpg/encrypted_part'
|
5
|
+
require 'schleuder/mail/gpg/inline_decrypted_message'
|
6
|
+
require 'schleuder/mail/gpg/gpgme_helper'
|
7
|
+
require 'schleuder/mail/gpg/signed_part'
|
8
|
+
require 'schleuder/mail/gpg/mime_signed_message'
|
9
|
+
require 'schleuder/mail/gpg/inline_signed_message'
|
10
|
+
|
1
11
|
module Mail
|
2
12
|
module Gpg
|
3
|
-
|
4
|
-
|
13
|
+
BEGIN_PGP_MESSAGE_MARKER = /^-----BEGIN PGP MESSAGE-----/
|
14
|
+
BEGIN_PGP_SIGNED_MESSAGE_MARKER = /^-----BEGIN PGP SIGNED MESSAGE-----/
|
15
|
+
|
16
|
+
# options are:
|
17
|
+
# :sign: sign message using the sender's private key
|
18
|
+
# :sign_as: sign using this key (give the corresponding email address or key fingerprint)
|
19
|
+
# :password: passphrase for the signing key
|
20
|
+
# :keys: A hash mapping recipient email addresses to public keys or public
|
21
|
+
# key ids. Imports any keys given here that are not already part of the
|
22
|
+
# local keychain before sending the mail.
|
23
|
+
# :always_trust: send encrypted mail to untrusted receivers, true by default
|
24
|
+
def self.encrypt(cleartext_mail, options = {})
|
25
|
+
construct_mail(cleartext_mail, options) do
|
26
|
+
receivers = []
|
27
|
+
receivers += cleartext_mail.to if cleartext_mail.to
|
28
|
+
receivers += cleartext_mail.cc if cleartext_mail.cc
|
29
|
+
receivers += cleartext_mail.bcc if cleartext_mail.bcc
|
30
|
+
|
31
|
+
if options[:sign_as]
|
32
|
+
options[:sign] = true
|
33
|
+
options[:signers] = options.delete(:sign_as)
|
34
|
+
elsif options[:sign]
|
35
|
+
options[:signers] = cleartext_mail.from
|
36
|
+
end
|
37
|
+
|
38
|
+
add_part VersionPart.new
|
39
|
+
add_part EncryptedPart.new(cleartext_mail,
|
40
|
+
options.merge({recipients: receivers}))
|
41
|
+
content_type "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=#{boundary}"
|
42
|
+
body.preamble = options[:preamble] || 'This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)'
|
5
43
|
|
6
|
-
def encrypt(cleartext_mail, options={})
|
7
|
-
encrypted_mail = encrypt_mailgpg(cleartext_mail, options)
|
8
44
|
if cleartext_mail.protected_headers_subject
|
9
|
-
|
45
|
+
subject cleartext_mail.protected_headers_subject
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.sign(cleartext_mail, options = {})
|
51
|
+
options[:sign_as] ||= cleartext_mail.from
|
52
|
+
construct_mail(cleartext_mail, options) do
|
53
|
+
to_be_signed = SignedPart.build(cleartext_mail)
|
54
|
+
add_part to_be_signed
|
55
|
+
add_part to_be_signed.sign(options)
|
56
|
+
|
57
|
+
content_type "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{boundary}"
|
58
|
+
body.preamble = options[:preamble] || 'This is an OpenPGP/MIME signed message (RFC 4880 and 3156)'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# options are:
|
63
|
+
# :verify: decrypt and verify
|
64
|
+
def self.decrypt(encrypted_mail, options = {})
|
65
|
+
if encrypted_mime?(encrypted_mail)
|
66
|
+
decrypt_pgp_mime(encrypted_mail, options)
|
67
|
+
elsif encrypted_inline?(encrypted_mail)
|
68
|
+
decrypt_pgp_inline(encrypted_mail, options)
|
69
|
+
else
|
70
|
+
raise EncodingError.new("Unsupported encryption format '#{encrypted_mail.content_type}'")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.signature_valid?(signed_mail, options = {})
|
75
|
+
if signed_mime?(signed_mail)
|
76
|
+
signature_valid_pgp_mime?(signed_mail, options)
|
77
|
+
elsif signed_inline?(signed_mail)
|
78
|
+
signature_valid_inline?(signed_mail, options)
|
79
|
+
else
|
80
|
+
raise EncodingError.new("Unsupported signature format '#{signed_mail.content_type}'")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# true if a mail is encrypted
|
85
|
+
def self.encrypted?(mail)
|
86
|
+
return true if encrypted_mime?(mail)
|
87
|
+
return true if encrypted_inline?(mail)
|
88
|
+
false
|
89
|
+
end
|
90
|
+
|
91
|
+
# true if a mail is signed.
|
92
|
+
#
|
93
|
+
# throws EncodingError if called on an encrypted mail (so only call this method if encrypted? is false)
|
94
|
+
def self.signed?(mail)
|
95
|
+
return true if signed_mime?(mail)
|
96
|
+
return true if signed_inline?(mail)
|
97
|
+
if encrypted?(mail)
|
98
|
+
raise EncodingError.new('Unable to determine signature on an encrypted mail, use :verify option on decrypt()')
|
99
|
+
end
|
100
|
+
false
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.construct_mail(cleartext_mail, options, &block)
|
104
|
+
Mail.new do
|
105
|
+
self.perform_deliveries = cleartext_mail.perform_deliveries
|
106
|
+
Mail::Gpg.copy_headers cleartext_mail, self
|
107
|
+
# necessary?
|
108
|
+
if cleartext_mail.message_id
|
109
|
+
header['Message-ID'] = cleartext_mail['Message-ID'].value
|
110
|
+
end
|
111
|
+
instance_eval &block
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# decrypts PGP/MIME (RFC 3156, section 4) encrypted mail
|
116
|
+
def self.decrypt_pgp_mime(encrypted_mail, options)
|
117
|
+
if encrypted_mail.parts.length < 2
|
118
|
+
raise EncodingError.new("RFC 3156 mandates exactly two body parts, found '#{encrypted_mail.parts.length}'")
|
119
|
+
end
|
120
|
+
if !VersionPart.is_version_part? encrypted_mail.parts[0]
|
121
|
+
raise EncodingError.new("RFC 3156 first part not a valid version part '#{encrypted_mail.parts[0]}'")
|
122
|
+
end
|
123
|
+
decrypted = DecryptedPart.new(encrypted_mail.parts[1], options)
|
124
|
+
Mail.new(decrypted.raw_source) do
|
125
|
+
# headers from the encrypted part (set by the initializer above) take
|
126
|
+
# precedence over those from the outer mail.
|
127
|
+
Mail::Gpg.copy_headers encrypted_mail, self, overwrite: false
|
128
|
+
verify_result decrypted.verify_result if options[:verify]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# decrypts inline PGP encrypted mail
|
133
|
+
def self.decrypt_pgp_inline(encrypted_mail, options)
|
134
|
+
InlineDecryptedMessage.setup(encrypted_mail, options)
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.verify(signed_mail, options = {})
|
138
|
+
if signed_mime?(signed_mail)
|
139
|
+
Mail::Gpg::MimeSignedMessage.setup signed_mail, options
|
140
|
+
elsif signed_inline?(signed_mail)
|
141
|
+
Mail::Gpg::InlineSignedMessage.setup signed_mail, options
|
142
|
+
else
|
143
|
+
signed_mail
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# check signature for PGP/MIME (RFC 3156, section 5) signed mail
|
148
|
+
def self.signature_valid_pgp_mime?(signed_mail, options)
|
149
|
+
# MUST contain exactly two body parts
|
150
|
+
if signed_mail.parts.length != 2
|
151
|
+
raise EncodingError.new("RFC 3156 mandates exactly two body parts, found '#{signed_mail.parts.length}'")
|
152
|
+
end
|
153
|
+
result, verify_result = SignPart.verify_signature(signed_mail.parts[0], signed_mail.parts[1], options)
|
154
|
+
signed_mail.verify_result = verify_result
|
155
|
+
return result
|
156
|
+
end
|
157
|
+
|
158
|
+
# check signature for inline signed mail
|
159
|
+
def self.signature_valid_inline?(signed_mail, options)
|
160
|
+
result = nil
|
161
|
+
if signed_mail.multipart?
|
162
|
+
signed_mail.parts.each do |part|
|
163
|
+
if signed_inline?(part)
|
164
|
+
if result.nil?
|
165
|
+
result = true
|
166
|
+
signed_mail.verify_result = []
|
167
|
+
end
|
168
|
+
result &= signature_valid_inline?(part, options)
|
169
|
+
signed_mail.verify_result << part.verify_result
|
170
|
+
end
|
171
|
+
end
|
172
|
+
else
|
173
|
+
result, verify_result = GpgmeHelper.inline_verify(signed_mail.body.to_s, options)
|
174
|
+
signed_mail.verify_result = verify_result
|
175
|
+
end
|
176
|
+
return result
|
177
|
+
end
|
178
|
+
|
179
|
+
# copies all header fields from mail in first argument to that given last
|
180
|
+
def self.copy_headers(from, to, overwrite: true)
|
181
|
+
from.header.fields.each do |field|
|
182
|
+
if overwrite || to.header[field.name].nil?
|
183
|
+
to.header[field.name] = field.value
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# check if PGP/MIME encrypted (RFC 3156)
|
189
|
+
def self.encrypted_mime?(mail)
|
190
|
+
mail.has_content_type? &&
|
191
|
+
mail.mime_type == 'multipart/encrypted' &&
|
192
|
+
mail.content_type_parameters[:protocol] == 'application/pgp-encrypted'
|
193
|
+
end
|
194
|
+
|
195
|
+
# check if inline PGP (i.e. if any parts of the mail includes
|
196
|
+
# the PGP MESSAGE marker)
|
197
|
+
def self.encrypted_inline?(mail)
|
198
|
+
begin
|
199
|
+
return true if mail.body.to_s =~ BEGIN_PGP_MESSAGE_MARKER
|
200
|
+
rescue
|
201
|
+
end
|
202
|
+
if mail.multipart?
|
203
|
+
mail.parts.each do |part|
|
204
|
+
begin
|
205
|
+
return true if part.body.to_s =~ BEGIN_PGP_MESSAGE_MARKER
|
206
|
+
rescue
|
207
|
+
end
|
208
|
+
return true if part.has_content_type? &&
|
209
|
+
/application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type &&
|
210
|
+
/.*\.(?:pgp|gpg|asc)$/ =~ part.content_type_parameters[:name] &&
|
211
|
+
part.content_type_parameters[:name] != 'signature.asc'
|
212
|
+
# that last condition above prevents false positives in case e.g.
|
213
|
+
# someone forwards a mime signed mail including signature.
|
214
|
+
end
|
215
|
+
end
|
216
|
+
false
|
217
|
+
end
|
218
|
+
|
219
|
+
# check if PGP/MIME signed (RFC 3156)
|
220
|
+
def self.signed_mime?(mail)
|
221
|
+
mail.has_content_type? &&
|
222
|
+
mail.mime_type == 'multipart/signed' &&
|
223
|
+
mail.content_type_parameters[:protocol] == 'application/pgp-signature'
|
224
|
+
end
|
225
|
+
|
226
|
+
# check if inline PGP (i.e. if any parts of the mail includes
|
227
|
+
# the PGP SIGNED marker)
|
228
|
+
def self.signed_inline?(mail)
|
229
|
+
begin
|
230
|
+
return true if mail.body.to_s =~ BEGIN_PGP_SIGNED_MESSAGE_MARKER
|
231
|
+
rescue
|
232
|
+
end
|
233
|
+
if mail.multipart?
|
234
|
+
mail.parts.each do |part|
|
235
|
+
begin
|
236
|
+
return true if part.body.to_s =~ BEGIN_PGP_SIGNED_MESSAGE_MARKER
|
237
|
+
rescue
|
238
|
+
end
|
10
239
|
end
|
11
|
-
encrypted_mail
|
12
240
|
end
|
13
|
-
|
241
|
+
false
|
242
|
+
end
|
14
243
|
end
|
15
244
|
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'schleuder/mail/gpg/delivery_handler'
|
2
|
+
require 'schleuder/mail/gpg/verify_result_attribute'
|
3
|
+
|
1
4
|
module Mail
|
2
5
|
# creates a Mail::Message likes schleuder
|
3
6
|
def self.create_message_to_list(msg, recipient, list)
|
@@ -13,11 +16,14 @@ module Mail
|
|
13
16
|
|
14
17
|
# TODO: Test if subclassing breaks integration of mail-gpg.
|
15
18
|
class Message
|
19
|
+
include Mail::Gpg::VerifyResultAttribute #for Mail::Gpg
|
20
|
+
|
16
21
|
attr_accessor :recipient
|
17
22
|
attr_accessor :original_message
|
18
23
|
attr_accessor :list
|
19
24
|
attr_accessor :protected_headers_subject
|
20
25
|
attr_writer :dynamic_pseudoheaders
|
26
|
+
attr_accessor :raise_encryption_errors # for Mail::Gpg
|
21
27
|
|
22
28
|
# TODO: This should be in initialize(), but I couldn't understand the
|
23
29
|
# strange errors about wrong number of arguments when overriding
|
@@ -154,7 +160,12 @@ module Mail
|
|
154
160
|
def signer
|
155
161
|
@signer ||= begin
|
156
162
|
if signing_key.present?
|
157
|
-
|
163
|
+
# Look for a subscription that matches the sending address, in case
|
164
|
+
# there're multiple subscriptions for the same key. As a fallback use
|
165
|
+
# the first subscription found.
|
166
|
+
sender_email = self.from.to_s.downcase
|
167
|
+
subscriptions = list.subscriptions.where(fingerprint: signing_key.fingerprint)
|
168
|
+
subscriptions.where(email: sender_email).first || subscriptions.first
|
158
169
|
end
|
159
170
|
end
|
160
171
|
end
|
@@ -436,36 +447,109 @@ module Mail
|
|
436
447
|
true
|
437
448
|
end
|
438
449
|
|
450
|
+
# turn on gpg encryption / set gpg options.
|
451
|
+
#
|
452
|
+
# options are:
|
453
|
+
#
|
454
|
+
# encrypt: encrypt the message. defaults to true
|
455
|
+
# sign: also sign the message. false by default
|
456
|
+
# sign_as: UIDs to sign the message with
|
457
|
+
#
|
458
|
+
# See Mail::Gpg methods encrypt and sign for more
|
459
|
+
# possible options
|
460
|
+
#
|
461
|
+
# mail.gpg encrypt: true
|
462
|
+
# mail.gpg encrypt: true, sign: true
|
463
|
+
# mail.gpg encrypt: true, sign_as: "other_address@host.com"
|
464
|
+
#
|
465
|
+
# sign-only mode is also supported:
|
466
|
+
# mail.gpg sign: true
|
467
|
+
# mail.gpg sign_as: 'jane@doe.com'
|
468
|
+
#
|
469
|
+
# To turn off gpg encryption use:
|
470
|
+
# mail.gpg false
|
471
|
+
#
|
472
|
+
def gpg(options = nil)
|
473
|
+
case options
|
474
|
+
when nil
|
475
|
+
@gpg
|
476
|
+
when false
|
477
|
+
@gpg = nil
|
478
|
+
if Mail::Gpg::DeliveryHandler == delivery_handler
|
479
|
+
self.delivery_handler = nil
|
480
|
+
end
|
481
|
+
nil
|
482
|
+
else
|
483
|
+
self.raise_encryption_errors = true if raise_encryption_errors.nil?
|
484
|
+
@gpg = options
|
485
|
+
self.delivery_handler ||= Mail::Gpg::DeliveryHandler
|
486
|
+
nil
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
# true if this mail is encrypted
|
491
|
+
def encrypted?
|
492
|
+
Mail::Gpg.encrypted?(self)
|
493
|
+
end
|
494
|
+
|
495
|
+
# returns the decrypted mail object.
|
496
|
+
#
|
497
|
+
# pass verify: true to verify signatures as well. The gpgme verification
|
498
|
+
# result will be available via decrypted_mail.verify_result
|
499
|
+
def decrypt(options = {})
|
500
|
+
Mail::Gpg.decrypt(self, options)
|
501
|
+
end
|
502
|
+
|
503
|
+
# true if this mail is signed (but not encrypted)
|
504
|
+
def signed?
|
505
|
+
Mail::Gpg.signed?(self)
|
506
|
+
end
|
507
|
+
|
508
|
+
# verify signatures. returns a new mail object with signatures removed and
|
509
|
+
# populated verify_result.
|
510
|
+
#
|
511
|
+
# verified = signed_mail.verify()
|
512
|
+
# verified.signature_valid?
|
513
|
+
# signers = mail.signatures.map{|sig| sig.from}
|
514
|
+
#
|
515
|
+
# use import_missing_keys: true in order to try to fetch and import
|
516
|
+
# unknown keys for signature validation
|
517
|
+
def verify(options = {})
|
518
|
+
Mail::Gpg.verify(self, options)
|
519
|
+
end
|
520
|
+
|
521
|
+
def repeat_validation!
|
522
|
+
new = self.original_message.dup.setup
|
523
|
+
self.verify_result = new.verify_result
|
524
|
+
@signatures = new.signatures
|
525
|
+
dynamic_pseudoheaders << new.dynamic_pseudoheaders
|
526
|
+
end
|
527
|
+
|
439
528
|
private
|
440
529
|
|
441
530
|
|
442
531
|
def extract_keywords(content_lines)
|
443
532
|
keywords = []
|
444
533
|
in_keyword_block = false
|
445
|
-
found_blank_line = false
|
446
534
|
content_lines.each_with_index do |line, i|
|
447
|
-
if
|
535
|
+
if line.blank?
|
536
|
+
# Swallow the line: before the actual content or keywords block begins we want to drop blank lines.
|
537
|
+
content_lines[i] = nil
|
538
|
+
# Stop interpreting the following line as argument to the previous keyword.
|
539
|
+
in_keyword_block = false
|
540
|
+
elsif match = line.match(/^x-([^:\s]*)[:\s]*(.*)/i)
|
448
541
|
keyword = match[1].strip.downcase
|
449
542
|
arguments = match[2].to_s.strip.downcase.split(/[,; ]{1,}/)
|
450
543
|
keywords << [keyword, arguments]
|
451
544
|
in_keyword_block = true
|
452
|
-
|
453
545
|
# Set this line to nil to have it stripped from the message.
|
454
546
|
content_lines[i] = nil
|
455
|
-
elsif line.blank? && keywords.any?
|
456
|
-
# Look for blank lines after the first keyword had been found.
|
457
|
-
# These might mark the end of the keywords-block — unless more keywords follow.
|
458
|
-
found_blank_line = true
|
459
|
-
# Swallow the line: before the actual content begins we want to drop blank lines.
|
460
|
-
content_lines[i] = nil
|
461
|
-
# Stop interpreting the following line as argument to the previous keyword.
|
462
|
-
in_keyword_block = false
|
463
547
|
elsif in_keyword_block == true
|
464
548
|
# Interpret line as arguments to the previous keyword.
|
465
549
|
keywords[-1][-1] += line.downcase.strip.split(/[,; ]{1,}/)
|
466
550
|
content_lines[i] = nil
|
467
|
-
|
468
|
-
# Any line
|
551
|
+
else
|
552
|
+
# Any other line stops the keyword parsing.
|
469
553
|
break
|
470
554
|
end
|
471
555
|
end
|
data/lib/schleuder/runner.rb
CHANGED
@@ -31,7 +31,13 @@ module Schleuder
|
|
31
31
|
end
|
32
32
|
|
33
33
|
error = run_filters('pre')
|
34
|
-
|
34
|
+
if error
|
35
|
+
if bounce?(error, @mail)
|
36
|
+
return error
|
37
|
+
else
|
38
|
+
return nil
|
39
|
+
end
|
40
|
+
end
|
35
41
|
|
36
42
|
begin
|
37
43
|
# This decrypts, verifies, etc.
|
@@ -48,7 +54,13 @@ module Schleuder
|
|
48
54
|
end
|
49
55
|
|
50
56
|
error = run_filters('post')
|
51
|
-
|
57
|
+
if error
|
58
|
+
if bounce?(error, @mail)
|
59
|
+
return error
|
60
|
+
else
|
61
|
+
return nil
|
62
|
+
end
|
63
|
+
end
|
52
64
|
|
53
65
|
if ! @mail.was_validly_signed?
|
54
66
|
logger.debug 'Message was not validly signed, adding subject_prefix_in'
|
@@ -85,16 +97,34 @@ module Schleuder
|
|
85
97
|
@list
|
86
98
|
end
|
87
99
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
100
|
+
def notify_admins(reason, original_message)
|
101
|
+
if list.bounces_notify_admins
|
102
|
+
list.logger.notify_admin reason, original_message, I18n.t('notice')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def bounce?(response, mail)
|
107
|
+
if list.bounces_drop_all
|
108
|
+
list.logger.debug 'Dropping bounce as configurated'
|
109
|
+
notify_admins(I18n.t('.bounces_drop_all'), mail.original_message)
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
|
113
|
+
list.bounces_drop_on_headers.each do |key, value|
|
114
|
+
if mail[key].to_s.match(/#{value}/i)
|
115
|
+
list.logger.debug "Incoming message header key '#{key}' matches value '#{value}': dropping the bounce."
|
116
|
+
notify_admins(I18n.t('.bounces_drop_on_headers', key: key, value: value), mail.original_message)
|
117
|
+
return false
|
95
118
|
end
|
96
|
-
return error
|
97
119
|
end
|
120
|
+
|
121
|
+
list.logger.debug 'Bouncing message'
|
122
|
+
notify_admins("#{I18n.t('.bounces_notify_admins')}\n\n#{response}", mail.original_message)
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
def run_filters(filter_type)
|
127
|
+
filters_runner(filter_type).run(@mail)
|
98
128
|
end
|
99
129
|
|
100
130
|
def filters_runner(filter_type)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class SksClient < Http
|
3
|
+
SKS_PATH = '/pks/lookup?&exact=on&op=get&options=mr&search=SEARCH_ARG'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def get(input)
|
7
|
+
super(url(input))
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def url(input)
|
13
|
+
arg = CGI.escape(input.gsub(/\s/, ''))
|
14
|
+
Conf.sks_keyserver + SKS_PATH.gsub('SEARCH_ARG', arg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/schleuder/version.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class VksClient < Http
|
3
|
+
VKS_PATH = '/vks/v1'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def get(type, input)
|
7
|
+
if type.to_s == 'fingerprint'
|
8
|
+
input = normalize_fingerprint(input)
|
9
|
+
end
|
10
|
+
super(url(type, input))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def normalize_fingerprint(input)
|
16
|
+
input.gsub(/^0x/, '').gsub(/\s/, '').upcase
|
17
|
+
end
|
18
|
+
|
19
|
+
def url(type, input)
|
20
|
+
"#{Conf.vks_keyserver}/#{VKS_PATH}/by-#{type}/#{CGI.escape(input)}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -14,7 +14,28 @@ class SchleuderApiDaemon < Sinatra::Base
|
|
14
14
|
if ! input.match('BEGIN PGP')
|
15
15
|
input = Base64.decode64(input)
|
16
16
|
end
|
17
|
-
|
17
|
+
@list = list(requested_list_id)
|
18
|
+
import_result = @list.import_key(input)
|
19
|
+
keys = []
|
20
|
+
messages = []
|
21
|
+
import_result.imports.each do |import_status|
|
22
|
+
if import_status.action == 'error'
|
23
|
+
messages << "The key with the fingerprint #{import_status.fingerprint} could not be imported for unknown reasons"
|
24
|
+
else
|
25
|
+
key = @list.gpg.find_distinct_key(import_status.fingerprint)
|
26
|
+
if key
|
27
|
+
keys << key_to_hash(key).merge({import_action: import_status.action})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
set_x_messages(messages)
|
32
|
+
# Use a Hash as single response object to stay more REST-like. (also,
|
33
|
+
# ActiveResource chokes if we return an array here).
|
34
|
+
# The 'type' attribute is only necessary to keep ActiveResource from
|
35
|
+
# complaining about "expected attributes to be able to convert to Hash",
|
36
|
+
# which for some reason is raised without it (or some other, similar
|
37
|
+
# attribute).
|
38
|
+
json({type: 'keys', keys: keys})
|
18
39
|
end
|
19
40
|
|
20
41
|
get '/check_keys.json' do
|
data/lib/schleuder.rb
CHANGED
@@ -15,15 +15,15 @@ require 'syslog/logger'
|
|
15
15
|
require 'logger'
|
16
16
|
require 'open3'
|
17
17
|
require 'socket'
|
18
|
+
require 'base64'
|
18
19
|
|
19
20
|
# Require mandatory libs. The database-layer-lib is required below.
|
20
|
-
require 'mail
|
21
|
+
require 'mail'
|
22
|
+
require 'gpgme'
|
21
23
|
require 'active_record'
|
22
24
|
require 'active_support'
|
23
25
|
require 'active_support/core_ext/string'
|
24
|
-
|
25
|
-
# An extra from mail-gpg
|
26
|
-
require 'hkp'
|
26
|
+
require 'typhoeus'
|
27
27
|
|
28
28
|
# Load schleuder
|
29
29
|
libdir = Pathname.new(__FILE__).dirname.realpath
|
@@ -34,13 +34,12 @@ $:.unshift libdir
|
|
34
34
|
require 'schleuder/mail/parts_list.rb'
|
35
35
|
require 'schleuder/mail/message.rb'
|
36
36
|
require 'schleuder/mail/gpg.rb'
|
37
|
-
require 'schleuder/mail/gpg/encrypted_part.rb'
|
38
|
-
require 'schleuder/mail/gpg/sign_part.rb'
|
39
37
|
require 'schleuder/gpgme/import_status.rb'
|
40
38
|
require 'schleuder/gpgme/key.rb'
|
41
39
|
require 'schleuder/gpgme/sub_key.rb'
|
42
40
|
require 'schleuder/gpgme/ctx.rb'
|
43
41
|
require 'schleuder/gpgme/user_id.rb'
|
42
|
+
require 'schleuder/gpgme/key_extractor'
|
44
43
|
|
45
44
|
# The Code[tm]
|
46
45
|
require 'schleuder/errors/base'
|
@@ -50,6 +49,10 @@ end
|
|
50
49
|
# Load schleuder/conf before the other classes, it defines constants!
|
51
50
|
require 'schleuder/conf'
|
52
51
|
require 'schleuder/version'
|
52
|
+
require 'schleuder/http'
|
53
|
+
require 'schleuder/key_fetcher'
|
54
|
+
require 'schleuder/vks_client'
|
55
|
+
require 'schleuder/sks_client'
|
53
56
|
require 'schleuder/logger_notifications'
|
54
57
|
require 'schleuder/logger'
|
55
58
|
require 'schleuder/listlogger'
|
@@ -66,6 +69,7 @@ require 'schleuder/runner'
|
|
66
69
|
require 'schleuder/list'
|
67
70
|
require 'schleuder/list_builder'
|
68
71
|
require 'schleuder/subscription'
|
72
|
+
require 'schleuder/email_key_importer'
|
69
73
|
|
70
74
|
# Setup
|
71
75
|
ENV['SCHLEUDER_CONFIG'] ||= '/etc/schleuder/schleuder.yml'
|
@@ -88,6 +92,6 @@ I18n.load_path += Dir["#{rootdir}/locales/*.yml"]
|
|
88
92
|
I18n.enforce_available_locales = true
|
89
93
|
I18n.default_locale = :en
|
90
94
|
|
91
|
-
File.umask(
|
95
|
+
File.umask(Schleuder::Conf.umask)
|
92
96
|
|
93
97
|
include Schleuder
|