schleuder 3.2.2 → 3.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -11
  3. data/Rakefile +18 -10
  4. data/bin/schleuder +2 -1
  5. data/bin/schleuder-api-daemon +3 -2
  6. data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +30 -0
  7. data/db/migrate/20180723173900_add_deliver_selfsent_to_list.rb +11 -0
  8. data/db/migrate/20190906194820_add_autocrypt_header_to_list.rb +11 -0
  9. data/db/schema.rb +4 -2
  10. data/etc/list-defaults.yml +13 -3
  11. data/etc/schleuder.yml +11 -0
  12. data/lib/schleuder-api-daemon.rb +9 -354
  13. data/lib/schleuder-api-daemon/helpers/schleuder-api-daemon-helper.rb +143 -0
  14. data/lib/schleuder-api-daemon/routes/key.rb +40 -0
  15. data/lib/schleuder-api-daemon/routes/list.rb +69 -0
  16. data/lib/schleuder-api-daemon/routes/status.rb +5 -0
  17. data/lib/schleuder-api-daemon/routes/subscription.rb +99 -0
  18. data/lib/schleuder-api-daemon/routes/version.rb +5 -0
  19. data/lib/schleuder.rb +12 -3
  20. data/lib/schleuder/cli.rb +33 -3
  21. data/lib/schleuder/cli/subcommand_fix.rb +1 -1
  22. data/lib/schleuder/conf.rb +7 -1
  23. data/lib/schleuder/errors/active_model_error.rb +2 -5
  24. data/lib/schleuder/errors/decryption_failed.rb +2 -7
  25. data/lib/schleuder/errors/key_adduid_failed.rb +1 -5
  26. data/lib/schleuder/errors/key_generation_failed.rb +1 -8
  27. data/lib/schleuder/errors/keyword_admin_only.rb +1 -5
  28. data/lib/schleuder/errors/list_not_found.rb +1 -5
  29. data/lib/schleuder/errors/listdir_problem.rb +2 -7
  30. data/lib/schleuder/errors/loading_list_settings_failed.rb +2 -5
  31. data/lib/schleuder/errors/message_empty.rb +1 -5
  32. data/lib/schleuder/errors/message_not_from_admin.rb +2 -5
  33. data/lib/schleuder/errors/message_sender_not_subscribed.rb +2 -5
  34. data/lib/schleuder/errors/message_too_big.rb +2 -5
  35. data/lib/schleuder/errors/message_unauthenticated.rb +1 -4
  36. data/lib/schleuder/errors/message_unencrypted.rb +2 -5
  37. data/lib/schleuder/errors/message_unsigned.rb +2 -5
  38. data/lib/schleuder/errors/too_many_keys.rb +1 -8
  39. data/lib/schleuder/filters/{request_filter.rb → post_decryption/10_request.rb} +0 -0
  40. data/lib/schleuder/filters/{max_message_size.rb → post_decryption/20_max_message_size.rb} +0 -0
  41. data/lib/schleuder/filters/{forward_filter.rb → post_decryption/30_forward_to_owner.rb} +0 -0
  42. data/lib/schleuder/filters/post_decryption/40_receive_admin_only.rb +10 -0
  43. data/lib/schleuder/filters/post_decryption/50_receive_authenticated_only.rb +10 -0
  44. data/lib/schleuder/filters/post_decryption/60_receive_signed_only.rb +10 -0
  45. data/lib/schleuder/filters/post_decryption/70_receive_encrypted_only.rb +10 -0
  46. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +10 -0
  47. data/lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb +21 -0
  48. data/lib/schleuder/filters/{bounces_filter.rb → pre_decryption/10_forward_bounce_to_admins.rb} +0 -0
  49. data/lib/schleuder/filters/{forward_incoming.rb → pre_decryption/20_forward_all_incoming_to_admins.rb} +0 -0
  50. data/lib/schleuder/filters/{send_key_filter.rb → pre_decryption/30_send_key.rb} +0 -0
  51. data/lib/schleuder/filters/{hotmail_message_filter.rb → pre_decryption/40_fix_exchange_messages.rb} +5 -3
  52. data/lib/schleuder/filters/{strip_alternative_filter.rb → pre_decryption/50_strip_html_from_alternative.rb} +1 -1
  53. data/lib/schleuder/filters_runner.rb +41 -31
  54. data/lib/schleuder/gpgme/ctx.rb +24 -3
  55. data/lib/schleuder/gpgme/import_status.rb +13 -7
  56. data/lib/schleuder/gpgme/key.rb +8 -0
  57. data/lib/schleuder/list.rb +26 -4
  58. data/lib/schleuder/logger_notifications.rb +8 -1
  59. data/lib/schleuder/mail/encrypted_part.rb +14 -0
  60. data/lib/schleuder/mail/gpg.rb +15 -0
  61. data/lib/schleuder/mail/message.rb +97 -49
  62. data/lib/schleuder/plugins/attach_listkey.rb +6 -10
  63. data/lib/schleuder/plugins/key_management.rb +34 -26
  64. data/lib/schleuder/plugins/resend.rb +14 -11
  65. data/lib/schleuder/plugins/subscription_management.rb +70 -3
  66. data/lib/schleuder/runner.rb +49 -10
  67. data/lib/schleuder/subscription.rb +5 -9
  68. data/lib/schleuder/validators/fingerprint_validator.rb +1 -1
  69. data/lib/schleuder/version.rb +1 -1
  70. data/locales/de.yml +101 -9
  71. data/locales/en.yml +107 -11
  72. metadata +72 -34
  73. data/lib/schleuder/errors/file_not_found.rb +0 -14
  74. data/lib/schleuder/errors/invalid_listname.rb +0 -13
  75. data/lib/schleuder/errors/list_exists.rb +0 -13
  76. data/lib/schleuder/errors/unknown_list_option.rb +0 -14
  77. data/lib/schleuder/filters/auth_filter.rb +0 -39
@@ -19,7 +19,7 @@ module GPGME
19
19
  case import_result.imports.size
20
20
  when 1
21
21
  import_status = import_result.imports.first
22
- if import_status.action == 'not imported'
22
+ if import_status.action == 'error'
23
23
  [nil, "Key #{import_status.fpr} could not be imported!"]
24
24
  else
25
25
  [import_status.fpr, nil]
@@ -103,7 +103,7 @@ module GPGME
103
103
  end
104
104
 
105
105
  def refresh_key(fingerprint)
106
- args = "#{keyserver_arg} --refresh-keys #{fingerprint}"
106
+ args = "#{keyserver_arg} #{import_filter_arg} --refresh-keys #{fingerprint}"
107
107
  gpgerr, gpgout, exitcode = self.class.gpgcli(args)
108
108
 
109
109
  if exitcode > 0
@@ -136,7 +136,8 @@ module GPGME
136
136
  arguments, error = fetch_key_gpg_arguments_for(input)
137
137
  return error if error
138
138
 
139
- gpgerr, gpgout, exitcode = self.class.gpgcli(arguments)
139
+ self.class.send_notice_if_gpg_does_not_know_import_filter
140
+ gpgerr, gpgout, exitcode = self.class.gpgcli("#{import_filter_arg} #{arguments}")
140
141
 
141
142
  # Unfortunately gpg doesn't exit with code > 0 if `--fetch-key` fails.
142
143
  if exitcode > 0 || gpgerr.grep(/ unable to fetch /).presence
@@ -270,5 +271,25 @@ module GPGME
270
271
  ""
271
272
  end
272
273
  end
274
+
275
+ def self.gpg_knows_import_filter?
276
+ sufficient_gpg_version?('2.1.15')
277
+ end
278
+
279
+ def import_filter_arg
280
+ if self.class.gpg_knows_import_filter?
281
+ %{ --import-filter drop-sig='sig_created_d > 0000-00-00'}
282
+ end
283
+ end
284
+
285
+ def self.send_notice_if_gpg_does_not_know_import_filter
286
+ if ! gpg_knows_import_filter?
287
+ Schleuder.logger.notify_superadmin(
288
+ subject: 'Schleuder installation problem',
289
+ message: "Your version of GnuPG is very old, please update!\n\nWith your version of GnuPG we can not protect your setup against signature flooding. Please update to at least version 2.1.15 to fix this problem. See <https://dkg.fifthhorseman.net/blog/openpgp-certificate-flooding.html> for details on the background."
290
+ )
291
+ ''
292
+ end
293
+ end
273
294
  end
274
295
  end
@@ -2,17 +2,23 @@ module GPGME
2
2
  class ImportStatus
3
3
  attr_reader :action
4
4
 
5
- # Unfortunately in initialize() @status and @result are not yet intialized.
5
+ # Unfortunately in initialize() @status and @result are not yet initialized.
6
6
  def set_action
7
- @action ||= if self.status > 0
8
- 'imported'
9
- elsif self.result == 0
10
- 'unchanged'
11
- else
7
+ @action ||= if self.result > 0
12
8
  # An error happened.
13
9
  # TODO: Give details by going through the list of errors in
14
10
  # "gpg-errors.h" and find out which is present here.
15
- 'not imported'
11
+ 'error'
12
+ else
13
+ # TODO: refactor with Ctx#translate_import_data
14
+ case self.status
15
+ when 0
16
+ 'unchanged'
17
+ when IMPORT_NEW
18
+ 'imported'
19
+ else
20
+ 'updated'
21
+ end
16
22
  end
17
23
  self
18
24
  end
@@ -57,6 +57,10 @@ module GPGME
57
57
  "#{self.to_s}\n\n#{export(armor: true).read}"
58
58
  end
59
59
 
60
+ def minimal
61
+ export(minimal: true).to_s
62
+ end
63
+
60
64
  # Force encoding, some databases save "ASCII-8BIT" as binary data.
61
65
  alias_method :orig_fingerprint, :fingerprint
62
66
  def fingerprint
@@ -208,5 +212,9 @@ module GPGME
208
212
  pinentry = File.join(ENV['SCHLEUDER_ROOT'], 'bin', 'pinentry-clearpassphrase')
209
213
  GPGME::Ctx.spawn_daemon('gpg-agent', "--use-standard-socket --pinentry-program #{pinentry}")
210
214
  end
215
+
216
+ def self.valid_fingerprint?(fp)
217
+ fp =~ Schleuder::Conf::FINGERPRINT_REGEXP
218
+ end
211
219
  end
212
220
  end
@@ -19,6 +19,7 @@ module Schleuder
19
19
  :receive_admin_only,
20
20
  :keep_msgid,
21
21
  :bounces_drop_all,
22
+ :deliver_selfsent,
22
23
  :bounces_notify_admins,
23
24
  :include_list_headers,
24
25
  :include_openpgp_header,
@@ -124,7 +125,7 @@ module Schleuder
124
125
 
125
126
  def import_key_and_find_fingerprint(key_material)
126
127
  return nil if key_material.blank?
127
-
128
+
128
129
  import_result = import_key(key_material)
129
130
  gpg.interpret_import_result(import_result)
130
131
  end
@@ -146,6 +147,16 @@ module Schleuder
146
147
  key.armored
147
148
  end
148
149
 
150
+ def key_minimal_base64_encoded(fingerprint=self.fingerprint)
151
+ key = keys(fingerprint).first
152
+
153
+ if key.blank?
154
+ return false
155
+ end
156
+
157
+ Base64.strict_encode64(key.minimal)
158
+ end
159
+
149
160
  def check_keys
150
161
  now = Time.now
151
162
  checkdate = now + (60 * 60 * 24 * 14) # two weeks
@@ -251,9 +262,8 @@ module Schleuder
251
262
  end
252
263
 
253
264
  def fingerprint=(arg)
254
- # Strip whitespace from incoming arg.
255
265
  if arg
256
- write_attribute(:fingerprint, arg.gsub(/\s*/, '').chomp)
266
+ write_attribute(:fingerprint, arg.gsub(/\s*/, '').gsub(/^0x/, '').chomp.upcase)
257
267
  end
258
268
  end
259
269
 
@@ -341,12 +351,24 @@ module Schleuder
341
351
  true
342
352
  end
343
353
 
344
- def send_to_subscriptions(mail)
354
+ def send_to_subscriptions(mail, incoming_mail=nil)
345
355
  logger.debug "Sending to subscriptions."
346
356
  mail.add_internal_footer!
347
357
  self.subscriptions.each do |subscription|
348
358
  begin
359
+
360
+ if ! subscription.delivery_enabled
361
+ logger.info "Not sending to #{subscription.email}: delivery is disabled."
362
+ next
363
+ end
364
+
365
+ if ! self.deliver_selfsent && incoming_mail.was_validly_signed? && ( subscription == incoming_mail.signer )
366
+ logger.info "Not sending to #{subscription.email}: delivery of self sent is disabled."
367
+ next
368
+ end
369
+
349
370
  subscription.send_mail(mail)
371
+
350
372
  rescue => exc
351
373
  msg = I18n.t('errors.delivery_error',
352
374
  { email: subscription.email, error: exc.to_s })
@@ -18,9 +18,14 @@ module Schleuder
18
18
  notify_admin(string, original_message)
19
19
  end
20
20
 
21
- def notify_admin(thing, original_message=nil, subject='Error')
21
+ def notify_superadmin(message:, original_message: nil, subject: 'Error')
22
+ notify_admin(message, original_message, subject, superadmin)
23
+ end
24
+
25
+ def notify_admin(thing, original_message=nil, subject='Error', recipients=nil)
22
26
  # Minimize using other classes here, we don't know what caused the error.
23
27
  msg_parts = convert_to_msg_parts(thing, original_message)
28
+ recipients ||= adminaddresses
24
29
  Array(adminaddresses).each do |address, key|
25
30
  mail = Mail.new
26
31
  mail.from = @from
@@ -37,6 +42,8 @@ module Schleuder
37
42
  gpg_opts.merge!(encrypt: true, keys: { address => key.fingerprint })
38
43
  end
39
44
  mail.gpg gpg_opts
45
+
46
+ mail.header['List-Id'] = "<#{@list.email.gsub('@', '.')}>"
40
47
  end
41
48
  mail.deliver
42
49
  end
@@ -0,0 +1,14 @@
1
+ module Mail
2
+ module Gpg
3
+ class EncryptedPart < Mail::Part
4
+ alias_method :initialize_mailgpg, :initialize
5
+
6
+ def initialize(cleartext_mail, options = {})
7
+ if cleartext_mail.protected_headers_subject
8
+ cleartext_mail.content_type_parameters['protected-headers'] = 'v1'
9
+ end
10
+ initialize_mailgpg(cleartext_mail, options)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ module Mail
2
+ module Gpg
3
+ class << self
4
+ alias_method :encrypt_mailgpg, :encrypt
5
+
6
+ def encrypt(cleartext_mail, options={})
7
+ encrypted_mail = encrypt_mailgpg(cleartext_mail, options)
8
+ if cleartext_mail.protected_headers_subject
9
+ encrypted_mail.subject = cleartext_mail.protected_headers_subject
10
+ end
11
+ encrypted_mail
12
+ end
13
+ end
14
+ end
15
+ end
@@ -16,6 +16,8 @@ module Mail
16
16
  attr_accessor :recipient
17
17
  attr_accessor :original_message
18
18
  attr_accessor :list
19
+ attr_accessor :protected_headers_subject
20
+ attr_writer :dynamic_pseudoheaders
19
21
 
20
22
  # TODO: This should be in initialize(), but I couldn't understand the
21
23
  # strange errors about wrong number of arguments when overriding
@@ -23,12 +25,9 @@ module Mail
23
25
  def setup
24
26
  if self.encrypted?
25
27
  new = self.decrypt(verify: true)
26
- ## Work around a bug in mail-gpg: when decrypting pgp/mime the
27
- ## Date-header is not copied.
28
- #new.date ||= self.date
29
28
  # Test if there's a signed multipart inside the ciphertext
30
29
  # ("encapsulated" format of pgp/mime).
31
- if new.signed?
30
+ if encapsulated_signed?(new)
32
31
  new = new.verify
33
32
  end
34
33
  elsif self.signed?
@@ -46,9 +45,20 @@ module Mail
46
45
  # might be gone (e.g. request-keywords that delete subscriptions or
47
46
  # keys).
48
47
  new.signer
49
- self.dynamic_pseudoheaders.each do |str|
50
- new.add_pseudoheader(str)
48
+ new.dynamic_pseudoheaders = self.dynamic_pseudoheaders.dup
49
+
50
+ # Store previously protected subject for later access.
51
+ # mail-gpg pulls headers from the decrypted mime parts "up" into the main
52
+ # headers, which reveals protected subjects.
53
+ if self.subject != new.subject
54
+ new.protected_headers_subject = self.subject.dup
55
+ end
56
+
57
+ # Delete the protected headers which might leak information.
58
+ if new.parts.first && new.parts.first.content_type == "text/rfc822-headers; protected-headers=v1"
59
+ new.parts.shift
51
60
  end
61
+
52
62
  new
53
63
  end
54
64
 
@@ -58,6 +68,7 @@ module Mail
58
68
  clean.gpg self.list.gpg_sign_options
59
69
  clean.from = list.email
60
70
  clean.subject = self.subject
71
+ clean.protected_headers_subject = self.protected_headers_subject
61
72
 
62
73
  clean.add_msgids(list, self)
63
74
  clean.add_list_headers(list)
@@ -69,6 +80,13 @@ module Mail
69
80
  clean.add_part new_part
70
81
  end
71
82
 
83
+ if self.protected_headers_subject.present?
84
+ new_part = Mail::Part.new
85
+ new_part.content_type = "text/rfc822-headers; protected-headers=v1"
86
+ new_part.body = "Subject: #{self.subject}\n"
87
+ clean.add_part new_part
88
+ end
89
+
72
90
  # Attach body or mime-parts in a new wrapper-part, to preserve the
73
91
  # original mime-structure.
74
92
  # We can't use self.to_s here — that includes all the headers we *don't*
@@ -187,11 +205,15 @@ module Mail
187
205
  @recipient.match(/-bounce@/).present? ||
188
206
  # Empty Return-Path
189
207
  self.return_path.to_s == '<>' ||
190
- # Auto-Submitted exists and does not equal 'no' and no cron header
191
- # present, as cron emails have the auto-submitted header.
208
+ # Auto-Submitted exists and does not equal 'no' and:
209
+ # - no cron header is present
210
+ # - no Jenkins job notification header is present
211
+ # as these emails have the auto-submitted header.
192
212
  ( self['Auto-Submitted'].present? && \
193
213
  self['Auto-Submitted'].to_s.downcase != 'no' && \
194
- !self['X-Cron-Env'].present?)
214
+ !self['X-Cron-Env'].present? && \
215
+ !self['X-Jenkins-Job'].present? && \
216
+ self.subject.to_s !~ /\A\*\*\* SECURITY information.*\*\*\*\Z/)
195
217
  end
196
218
 
197
219
  def keywords
@@ -203,15 +225,19 @@ module Mail
203
225
  end
204
226
 
205
227
  @keywords = []
228
+ look_for_keywords = true
206
229
  lines = part.decoded.lines.map do |line|
207
230
  # TODO: Find multiline arguments (add-key). Currently add-key has to
208
231
  # read the whole body and hope for the best.
209
- if line.match(/^x-([^:\s]*)[:\s]*(.*)/i)
210
- command = $1.strip.downcase
211
- arguments = $2.to_s.strip.downcase.split(/[,; ]{1,}/)
232
+ if look_for_keywords && (m = line.match(/^x-([^:\s]*)[:\s]*(.*)/i))
233
+ command = m[1].strip.downcase
234
+ arguments = m[2].to_s.strip.downcase.split(/[,; ]{1,}/)
212
235
  @keywords << [command, arguments]
213
236
  nil
214
237
  else
238
+ if look_for_keywords && line.match(/\S+/i)
239
+ look_for_keywords = false
240
+ end
215
241
  line
216
242
  end
217
243
  end
@@ -221,11 +247,11 @@ module Mail
221
247
  # decide itself how to encode, it works. If we don't, some
222
248
  # character-sequences are not properly re-encoded.
223
249
  part.content_transfer_encoding = nil
224
- # Make the converted strings (now UTF-8) match what mime-part's headers say,
225
- # fall back to US-ASCII if none is set.
226
- # https://tools.ietf.org/html/rfc2046#section-4.1.2
227
- # -> Default charset is US-ASCII
228
- part.body = lines.compact.join.encode(part.charset||'US-ASCII')
250
+
251
+ # Set the right charset on the now parsed body
252
+ new_body = lines.compact.join
253
+ part.charset = new_body.encoding.to_s
254
+ part.body = new_body
229
255
 
230
256
  @keywords
231
257
  end
@@ -243,33 +269,20 @@ module Mail
243
269
  end
244
270
 
245
271
  def add_pseudoheader(string_or_key, value=nil)
246
- @dynamic_pseudoheaders ||= []
247
- if value.present?
248
- @dynamic_pseudoheaders << make_pseudoheader(string_or_key, value)
249
- else
250
- @dynamic_pseudoheaders << string_or_key.to_s
251
- end
272
+ dynamic_pseudoheaders << make_pseudoheader(string_or_key, value)
252
273
  end
253
274
 
254
275
  def make_pseudoheader(key, value)
255
- "#{key.to_s.camelize}: #{value.to_s}"
276
+ output = "#{key.to_s.camelize}: #{value.to_s}"
277
+ # wrap lines after 76 with 2 indents
278
+ output.gsub(/(.{1,76})( +|$)\n?/, " \\1\n").chomp.lstrip
256
279
  end
257
280
 
258
281
  def dynamic_pseudoheaders
259
- @dynamic_pseudoheaders || []
282
+ @dynamic_pseudoheaders ||= []
260
283
  end
261
284
 
262
- def standard_pseudoheaders(list)
263
- if @standard_pseudoheaders.present?
264
- return @standard_pseudoheaders
265
- else
266
- @standard_pseudoheaders = []
267
- end
268
-
269
- Array(list.headers_to_meta).each do |field|
270
- @standard_pseudoheaders << make_pseudoheader(field.to_s, self.header[field.to_s])
271
- end
272
-
285
+ def signature_state
273
286
  # Careful to add information about the incoming signature. GPGME
274
287
  # throws exceptions if it doesn't know the key.
275
288
  if self.signature.present?
@@ -277,28 +290,48 @@ module Mail
277
290
  # for that manually and provide our own fallback. (Calling
278
291
  # `signature.key` results in an EOFError in that case.)
279
292
  if signing_key.present?
280
- msg = signature.to_s
293
+ signature_state = signature.to_s
281
294
  else
282
- # TODO: I18n
283
- msg = "Unknown signature by unknown key 0x#{self.signature.fingerprint}"
295
+ signature_state = I18n.t("signature_states.unknown", fingerprint: self.signature.fingerprint)
284
296
  end
285
297
  else
286
- # TODO: I18n
287
- msg = "Unsigned"
298
+ signature_state = I18n.t("signature_states.unsigned")
299
+ end
300
+ signature_state
301
+ end
302
+
303
+ def encryption_state
304
+ if was_encrypted?
305
+ encryption_state = I18n.t("encryption_states.encrypted")
306
+ else
307
+ encryption_state = I18n.t("encryption_states.unencrypted")
308
+ end
309
+ encryption_state
310
+ end
311
+
312
+ def standard_pseudoheaders(list)
313
+ if @standard_pseudoheaders.present?
314
+ return @standard_pseudoheaders
315
+ else
316
+ @standard_pseudoheaders = []
317
+ end
318
+
319
+ Array(list.headers_to_meta).each do |field|
320
+ value = case field.to_s
321
+ when 'sig' then signature_state
322
+ when 'enc' then encryption_state
323
+ else self.header[field.to_s]
324
+ end
325
+ @standard_pseudoheaders << make_pseudoheader(field.to_s, value)
288
326
  end
289
- @standard_pseudoheaders << make_pseudoheader(:sig, msg)
290
327
 
291
- # TODO: I18n
292
- @standard_pseudoheaders << make_pseudoheader(
293
- :enc,
294
- was_encrypted? ? 'Encrypted' : 'Unencrypted'
295
- )
296
328
 
297
329
  @standard_pseudoheaders
298
330
  end
299
331
 
300
332
  def pseudoheaders(list)
301
- (standard_pseudoheaders(list) + dynamic_pseudoheaders).flatten.join("\n") + "\n"
333
+ separator = '------------------------------------------------------------------------------'
334
+ (standard_pseudoheaders(list) + dynamic_pseudoheaders).flatten.join("\n") + "\n" + separator + "\n"
302
335
  end
303
336
 
304
337
  def add_msgids(list, orig)
@@ -313,10 +346,18 @@ module Mail
313
346
  end
314
347
 
315
348
  def add_list_headers(list)
349
+ if list.include_autocrypt_header
350
+ # Inject whitespaces, to let Mail break the string at these points
351
+ # leading to correct wrapping.
352
+ keydata = list.key_minimal_base64_encoded.gsub(/(.{78})/, '\1 ')
353
+
354
+ self['Autocrypt'] = "addr=#{list.email}; prefer-encrypt=mutual; keydata=#{keydata}"
355
+ end
356
+
316
357
  if list.include_list_headers
317
358
  self['List-Id'] = "<#{list.email.gsub('@', '.')}>"
318
359
  self['List-Owner'] = "<mailto:#{list.owner_address}> (Use list's public key)"
319
- self['List-Help'] = '<https://schleuder.nadir.org/>'
360
+ self['List-Help'] = '<https://schleuder.org/>'
320
361
 
321
362
  postmsg = if list.receive_admin_only
322
363
  "NO (Admins only)"
@@ -410,6 +451,13 @@ module Mail
410
451
 
411
452
  private
412
453
 
454
+ # mail.signed? throws an error if it finds
455
+ # pgp boundaries, so we must use the Mail::Gpg
456
+ # methods.
457
+ def encapsulated_signed?(mail)
458
+ (mail.verify_result.nil? || mail.verify_result.signatures.empty?) && \
459
+ (Mail::Gpg.signed_mime?(mail) || Mail::Gpg.signed_inline?(mail))
460
+ end
413
461
 
414
462
  def add_footer!(footer_attribute)
415
463
  if self.list.blank? || self.list.send(footer_attribute).to_s.empty?