schleuder 4.0.3 → 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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -9
  3. data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +1 -1
  4. data/db/migrate/20220910170110_add_key_auto_import_from_email.rb +11 -0
  5. data/db/schema.rb +6 -6
  6. data/etc/list-defaults.yml +16 -0
  7. data/etc/schleuder.yml +29 -11
  8. data/lib/schleuder/cli.rb +14 -1
  9. data/lib/schleuder/conf.rb +23 -3
  10. data/lib/schleuder/email_key_importer.rb +91 -0
  11. data/lib/schleuder/filters/post_decryption/35_key_auto_import_from_attachments.rb +21 -0
  12. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +1 -1
  13. data/lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb +36 -4
  14. data/lib/schleuder/filters/pre_decryption/60_key_auto_import_from_autocrypt_header.rb +9 -0
  15. data/lib/schleuder/gpgme/ctx.rb +34 -93
  16. data/lib/schleuder/gpgme/key.rb +1 -1
  17. data/lib/schleuder/gpgme/key_extractor.rb +30 -0
  18. data/lib/schleuder/http.rb +56 -0
  19. data/lib/schleuder/key_fetcher.rb +89 -0
  20. data/lib/schleuder/keyword_handlers/key_management.rb +2 -2
  21. data/lib/schleuder/keyword_handlers/subscription_management.rb +19 -3
  22. data/lib/schleuder/list.rb +26 -10
  23. data/lib/schleuder/list_builder.rb +1 -1
  24. data/lib/schleuder/logger.rb +1 -1
  25. data/lib/schleuder/mail/gpg/decrypted_part.rb +20 -0
  26. data/lib/schleuder/mail/gpg/delivery_handler.rb +38 -0
  27. data/lib/schleuder/mail/gpg/encrypted_part.rb +29 -5
  28. data/lib/schleuder/mail/gpg/gpgme_ext.rb +8 -0
  29. data/lib/schleuder/mail/gpg/gpgme_helper.rb +155 -0
  30. data/lib/schleuder/mail/gpg/inline_decrypted_message.rb +82 -0
  31. data/lib/schleuder/mail/gpg/inline_signed_message.rb +73 -0
  32. data/lib/schleuder/mail/gpg/mime_signed_message.rb +28 -0
  33. data/lib/schleuder/mail/gpg/missing_keys_error.rb +6 -0
  34. data/lib/schleuder/mail/gpg/sign_part.rb +19 -9
  35. data/lib/schleuder/mail/gpg/signed_part.rb +37 -0
  36. data/lib/schleuder/mail/gpg/verified_part.rb +10 -0
  37. data/lib/schleuder/mail/gpg/verify_result_attribute.rb +32 -0
  38. data/lib/schleuder/mail/gpg/version_part.rb +22 -0
  39. data/lib/schleuder/mail/gpg.rb +236 -7
  40. data/lib/schleuder/mail/message.rb +98 -14
  41. data/lib/schleuder/sks_client.rb +18 -0
  42. data/lib/schleuder/version.rb +1 -1
  43. data/lib/schleuder/vks_client.rb +24 -0
  44. data/lib/schleuder-api-daemon/routes/key.rb +22 -1
  45. data/lib/schleuder.rb +11 -7
  46. data/locales/de.yml +22 -3
  47. data/locales/en.yml +22 -3
  48. metadata +72 -24
@@ -0,0 +1,73 @@
1
+ require 'schleuder/mail/gpg/verified_part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class InlineSignedMessage < Mail::Message
6
+
7
+ def self.setup(signed_mail, options = {})
8
+ if signed_mail.multipart?
9
+ self.new do
10
+ global_verify_result = []
11
+ signed_mail.header.fields.each do |field|
12
+ header[field.name] = field.value
13
+ end
14
+ signed_mail.parts.each do |part|
15
+ if Mail::Gpg.signed_inline?(part)
16
+ signed_text = part.body.to_s
17
+ success, vr = GpgmeHelper.inline_verify(signed_text, options)
18
+ p = VerifiedPart.new(part)
19
+ if success
20
+ p.body self.class.strip_inline_signature signed_text
21
+ end
22
+ p.verify_result vr
23
+ global_verify_result << vr
24
+ add_part p
25
+ else
26
+ add_part part
27
+ end
28
+ end
29
+ verify_result global_verify_result
30
+ end
31
+ else
32
+ self.new do
33
+ signed_mail.header.fields.each do |field|
34
+ header[field.name] = field.value
35
+ end
36
+ signed_text = signed_mail.body.to_s
37
+ success, vr = GpgmeHelper.inline_verify(signed_text, options)
38
+ if success
39
+ body self.class.strip_inline_signature signed_text
40
+ else
41
+ body signed_text
42
+ end
43
+ verify_result vr
44
+ end
45
+ end
46
+ end
47
+
48
+ END_SIGNED_TEXT = '-----END PGP SIGNED MESSAGE-----'
49
+ END_SIGNED_TEXT_RE = /^#{END_SIGNED_TEXT}\s*$/
50
+ INLINE_SIG_RE = Regexp.new('^-----BEGIN PGP SIGNATURE-----\s*$.*^-----END PGP SIGNATURE-----\s*$', Regexp::MULTILINE)
51
+ BEGIN_SIG_RE = /^(-----BEGIN PGP SIGNATURE-----)\s*$/
52
+
53
+
54
+ # utility method to remove inline signature and related pgp markers
55
+ def self.strip_inline_signature(signed_text)
56
+ if signed_text =~ INLINE_SIG_RE
57
+ signed_text = signed_text.dup
58
+ if signed_text !~ END_SIGNED_TEXT_RE
59
+ # insert the 'end of signed text' marker in case it is missing
60
+ signed_text = signed_text.gsub BEGIN_SIG_RE, "-----END PGP SIGNED MESSAGE-----\n\\1"
61
+ end
62
+ signed_text.gsub! INLINE_SIG_RE, ''
63
+ signed_text.strip!
64
+ end
65
+ # Strip possible inline-"headers" (e.g. "Hash: SHA256", or "Comment: something").
66
+ signed_text.gsub(/(.*^-----BEGIN PGP SIGNED MESSAGE-----\n)(.*?)^$(.+)/m, '\1\3')
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+
73
+
@@ -0,0 +1,28 @@
1
+ require 'schleuder/mail/gpg/verified_part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class MimeSignedMessage < Mail::Message
6
+
7
+ def self.setup(signed_mail, options = {})
8
+ content_part, signature = signed_mail.parts
9
+ success, vr = SignPart.verify_signature(content_part, signature, options)
10
+ self.new do
11
+ verify_result vr
12
+ signed_mail.header.fields.each do |field|
13
+ header[field.name] = field.value
14
+ end
15
+ content_part.header.fields.each do |field|
16
+ header[field.name] = field.value
17
+ end
18
+ if content_part.multipart?
19
+ content_part.parts.each{|part| add_part part}
20
+ else
21
+ body content_part.body.raw_source
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,6 @@
1
+ module Mail
2
+ module Gpg
3
+ class MissingKeysError < StandardError
4
+ end
5
+ end
6
+ end
@@ -1,15 +1,26 @@
1
1
  module Mail
2
2
  module Gpg
3
3
  class SignPart < Mail::Part
4
- # Copied verbatim from mail-gpg v.0.4.2. This code was changed in
5
- # <https://github.com/jkraemer/mail-gpg/commit/5fded41ccee4a58f848a2f8e7bd53d11236f8984>,
6
- # which breaks verifying some encapsulated (signed-then-encrypted)
7
- # messages. See
8
- # <https://github.com/jkraemer/mail-gpg/pull/40#issue-95776382> for
9
- # details.
4
+
5
+ def initialize(cleartext_mail, options = {})
6
+ signature = GpgmeHelper.sign(cleartext_mail.encoded, options)
7
+ super() do
8
+ body signature.to_s
9
+ content_type "application/pgp-signature; name=\"signature.asc\""
10
+ content_disposition 'attachment; filename="signature.asc"'
11
+ content_description 'OpenPGP digital signature'
12
+ end
13
+ end
14
+
15
+ # true if all signatures are valid
16
+ def self.signature_valid?(plain_part, signature_part, options = {})
17
+ verify_signature(plain_part, signature_part, options)[0]
18
+ end
19
+
20
+ # will return [success(boolean), verify_result(as returned by gpgme)]
10
21
  def self.verify_signature(plain_part, signature_part, options = {})
11
22
  if !(signature_part.has_content_type? &&
12
- ('application/pgp-signature' == signature_part.mime_type))
23
+ ('application/pgp-signature' == signature_part.mime_type))
13
24
  return false
14
25
  end
15
26
 
@@ -19,7 +30,7 @@ module Mail
19
30
  plaintext = [ plain_part.header.raw_source,
20
31
  "\r\n\r\n",
21
32
  plain_part.body.raw_source
22
- ].join
33
+ ].join
23
34
  else
24
35
  plaintext = plain_part.encoded
25
36
  end
@@ -30,4 +41,3 @@ module Mail
30
41
  end
31
42
  end
32
43
  end
33
-
@@ -0,0 +1,37 @@
1
+ require 'mail/part'
2
+ require 'schleuder/mail/gpg/sign_part'
3
+
4
+ module Mail
5
+ module Gpg
6
+
7
+ class SignedPart < Mail::Part
8
+
9
+ def self.build(cleartext_mail)
10
+ new do
11
+ if cleartext_mail.body.multipart?
12
+ if cleartext_mail.content_type =~ /^(multipart[^;]+)/
13
+ # preserve multipart/alternative etc
14
+ content_type $1
15
+ else
16
+ content_type 'multipart/mixed'
17
+ end
18
+ cleartext_mail.body.parts.each do |p|
19
+ add_part p
20
+ end
21
+ else
22
+ content_type cleartext_mail.content_type
23
+ body cleartext_mail.body.raw_source
24
+ end
25
+ end
26
+ end
27
+
28
+ def sign(options)
29
+ SignPart.new(self, options)
30
+ end
31
+
32
+
33
+ end
34
+
35
+
36
+ end
37
+ end
@@ -0,0 +1,10 @@
1
+ require 'schleuder/mail/gpg/verify_result_attribute'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class VerifiedPart < Mail::Part
6
+ include VerifyResultAttribute
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,32 @@
1
+ module Mail
2
+ module Gpg
3
+ module VerifyResultAttribute
4
+
5
+ # the result of signature verification, as provided by GPGME
6
+ def verify_result(result = nil)
7
+ if result
8
+ self.verify_result = result
9
+ else
10
+ @verify_result
11
+ end
12
+ end
13
+
14
+ def verify_result=(result)
15
+ @verify_result = result
16
+ end
17
+
18
+ # checks validity of signatures (true / false)
19
+ def signature_valid?
20
+ sigs = self.signatures
21
+ sigs.any? && sigs.all?{|s| s.valid?}
22
+ end
23
+
24
+ # list of all signatures from verify_result
25
+ def signatures
26
+ [verify_result].flatten.compact.map do |vr|
27
+ vr.signatures
28
+ end.flatten.compact
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ require 'mail/part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class VersionPart < Mail::Part
6
+ VERSION_1 = 'Version: 1'
7
+ CONTENT_TYPE = 'application/pgp-encrypted'
8
+ CONTENT_DESC = 'PGP/MIME Versions Identification'
9
+
10
+ def initialize(*args)
11
+ super
12
+ body VERSION_1
13
+ content_type CONTENT_TYPE
14
+ content_description CONTENT_DESC
15
+ end
16
+
17
+ def self.is_version_part?(part)
18
+ part.mime_type == CONTENT_TYPE && part.body =~ /#{VERSION_1}/
19
+ end
20
+ end
21
+ end
22
+ end
@@ -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
- class << self
4
- alias_method :encrypt_mailgpg, :encrypt
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
- encrypted_mail.subject = cleartext_mail.protected_headers_subject
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
- end
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
- list.subscriptions.where(fingerprint: signing_key.fingerprint).first
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 match = line.match(/^x-([^:\s]*)[:\s]*(.*)/i)
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
- elsif found_blank_line
468
- # Any line that isn't blank and does not start with "x-" stops the keyword parsing.
551
+ else
552
+ # Any other line stops the keyword parsing.
469
553
  break
470
554
  end
471
555
  end
@@ -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