schleuder 3.2.2 → 3.3.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -10
  3. data/Rakefile +16 -8
  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/schema.rb +2 -2
  8. data/etc/list-defaults.yml +4 -2
  9. data/etc/schleuder.yml +11 -0
  10. data/lib/schleuder-api-daemon.rb +9 -354
  11. data/lib/schleuder-api-daemon/helpers/schleuder-api-daemon-helper.rb +143 -0
  12. data/lib/schleuder-api-daemon/routes/key.rb +40 -0
  13. data/lib/schleuder-api-daemon/routes/list.rb +69 -0
  14. data/lib/schleuder-api-daemon/routes/status.rb +5 -0
  15. data/lib/schleuder-api-daemon/routes/subscription.rb +99 -0
  16. data/lib/schleuder-api-daemon/routes/version.rb +5 -0
  17. data/lib/schleuder.rb +2 -3
  18. data/lib/schleuder/cli.rb +24 -0
  19. data/lib/schleuder/cli/subcommand_fix.rb +1 -1
  20. data/lib/schleuder/conf.rb +7 -1
  21. data/lib/schleuder/errors/active_model_error.rb +2 -5
  22. data/lib/schleuder/errors/decryption_failed.rb +2 -7
  23. data/lib/schleuder/errors/key_adduid_failed.rb +1 -5
  24. data/lib/schleuder/errors/key_generation_failed.rb +1 -8
  25. data/lib/schleuder/errors/keyword_admin_only.rb +1 -5
  26. data/lib/schleuder/errors/list_not_found.rb +1 -5
  27. data/lib/schleuder/errors/listdir_problem.rb +2 -7
  28. data/lib/schleuder/errors/loading_list_settings_failed.rb +2 -5
  29. data/lib/schleuder/errors/message_empty.rb +1 -5
  30. data/lib/schleuder/errors/message_not_from_admin.rb +2 -5
  31. data/lib/schleuder/errors/message_sender_not_subscribed.rb +2 -5
  32. data/lib/schleuder/errors/message_too_big.rb +2 -5
  33. data/lib/schleuder/errors/message_unauthenticated.rb +1 -4
  34. data/lib/schleuder/errors/message_unencrypted.rb +2 -5
  35. data/lib/schleuder/errors/message_unsigned.rb +2 -5
  36. data/lib/schleuder/errors/too_many_keys.rb +1 -8
  37. data/lib/schleuder/filters/{request_filter.rb → post_decryption/10_request.rb} +0 -0
  38. data/lib/schleuder/filters/{max_message_size.rb → post_decryption/20_max_message_size.rb} +0 -0
  39. data/lib/schleuder/filters/{forward_filter.rb → post_decryption/30_forward_to_owner.rb} +0 -0
  40. data/lib/schleuder/filters/post_decryption/40_receive_admin_only.rb +10 -0
  41. data/lib/schleuder/filters/post_decryption/50_receive_authenticated_only.rb +10 -0
  42. data/lib/schleuder/filters/post_decryption/60_receive_signed_only.rb +10 -0
  43. data/lib/schleuder/filters/post_decryption/70_receive_encrypted_only.rb +10 -0
  44. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +10 -0
  45. data/lib/schleuder/filters/{bounces_filter.rb → pre_decryption/10_forward_bounce_to_admins.rb} +0 -0
  46. data/lib/schleuder/filters/{forward_incoming.rb → pre_decryption/20_forward_all_incoming_to_admins.rb} +0 -0
  47. data/lib/schleuder/filters/{send_key_filter.rb → pre_decryption/30_send_key.rb} +0 -0
  48. data/lib/schleuder/filters/{hotmail_message_filter.rb → pre_decryption/40_fix_exchange_messages.rb} +5 -3
  49. data/lib/schleuder/filters/{strip_alternative_filter.rb → pre_decryption/50_strip_html_from_alternative.rb} +1 -1
  50. data/lib/schleuder/filters_runner.rb +41 -31
  51. data/lib/schleuder/gpgme/ctx.rb +1 -1
  52. data/lib/schleuder/gpgme/import_status.rb +13 -7
  53. data/lib/schleuder/gpgme/key.rb +4 -0
  54. data/lib/schleuder/list.rb +7 -4
  55. data/lib/schleuder/mail/encrypted_part.rb +14 -0
  56. data/lib/schleuder/mail/gpg.rb +15 -0
  57. data/lib/schleuder/mail/message.rb +70 -30
  58. data/lib/schleuder/plugins/key_management.rb +32 -7
  59. data/lib/schleuder/plugins/subscription_management.rb +70 -3
  60. data/lib/schleuder/runner.rb +19 -8
  61. data/lib/schleuder/subscription.rb +5 -9
  62. data/lib/schleuder/validators/fingerprint_validator.rb +1 -1
  63. data/lib/schleuder/version.rb +1 -1
  64. data/locales/de.yml +96 -8
  65. data/locales/en.yml +102 -10
  66. metadata +48 -27
  67. data/lib/schleuder/errors/file_not_found.rb +0 -14
  68. data/lib/schleuder/errors/invalid_listname.rb +0 -13
  69. data/lib/schleuder/errors/list_exists.rb +0 -13
  70. data/lib/schleuder/errors/unknown_list_option.rb +0 -14
  71. 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]
@@ -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
@@ -208,5 +208,9 @@ module GPGME
208
208
  pinentry = File.join(ENV['SCHLEUDER_ROOT'], 'bin', 'pinentry-clearpassphrase')
209
209
  GPGME::Ctx.spawn_daemon('gpg-agent', "--use-standard-socket --pinentry-program #{pinentry}")
210
210
  end
211
+
212
+ def self.valid_fingerprint?(fp)
213
+ fp =~ Schleuder::Conf::FINGERPRINT_REGEXP
214
+ end
211
215
  end
212
216
  end
@@ -124,7 +124,7 @@ module Schleuder
124
124
 
125
125
  def import_key_and_find_fingerprint(key_material)
126
126
  return nil if key_material.blank?
127
-
127
+
128
128
  import_result = import_key(key_material)
129
129
  gpg.interpret_import_result(import_result)
130
130
  end
@@ -251,9 +251,8 @@ module Schleuder
251
251
  end
252
252
 
253
253
  def fingerprint=(arg)
254
- # Strip whitespace from incoming arg.
255
254
  if arg
256
- write_attribute(:fingerprint, arg.gsub(/\s*/, '').chomp)
255
+ write_attribute(:fingerprint, arg.gsub(/\s*/, '').gsub(/^0x/, '').chomp.upcase)
257
256
  end
258
257
  end
259
258
 
@@ -346,7 +345,11 @@ module Schleuder
346
345
  mail.add_internal_footer!
347
346
  self.subscriptions.each do |subscription|
348
347
  begin
349
- subscription.send_mail(mail)
348
+ if subscription.delivery_enabled
349
+ subscription.send_mail(mail)
350
+ else
351
+ logger.info "Not sending to #{subscription.email}: delivery is disabled."
352
+ end
350
353
  rescue => exc
351
354
  msg = I18n.t('errors.delivery_error',
352
355
  { email: subscription.email, error: exc.to_s })
@@ -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,7 @@ module Mail
16
16
  attr_accessor :recipient
17
17
  attr_accessor :original_message
18
18
  attr_accessor :list
19
+ attr_accessor :protected_headers_subject
19
20
 
20
21
  # TODO: This should be in initialize(), but I couldn't understand the
21
22
  # strange errors about wrong number of arguments when overriding
@@ -23,12 +24,9 @@ module Mail
23
24
  def setup
24
25
  if self.encrypted?
25
26
  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
27
  # Test if there's a signed multipart inside the ciphertext
30
28
  # ("encapsulated" format of pgp/mime).
31
- if new.signed?
29
+ if encapsulated_signed?(new)
32
30
  new = new.verify
33
31
  end
34
32
  elsif self.signed?
@@ -49,6 +47,20 @@ module Mail
49
47
  self.dynamic_pseudoheaders.each do |str|
50
48
  new.add_pseudoheader(str)
51
49
  end
50
+
51
+ # Store previously protected subject for later access.
52
+ # mail-gpg pulls headers from the decrypted mime parts "up" into the main
53
+ # headers, which reveals protected subjects.
54
+ if self.subject != new.subject
55
+ new.protected_headers_subject = self.subject.dup
56
+
57
+ # Delete the protected headers which might leak information.
58
+ if new.parts.first.content_type == "text/rfc822-headers; protected-headers=v1"
59
+ new.parts.shift
60
+ end
61
+ end
62
+
63
+
52
64
  new
53
65
  end
54
66
 
@@ -58,6 +70,7 @@ module Mail
58
70
  clean.gpg self.list.gpg_sign_options
59
71
  clean.from = list.email
60
72
  clean.subject = self.subject
73
+ clean.protected_headers_subject = self.protected_headers_subject
61
74
 
62
75
  clean.add_msgids(list, self)
63
76
  clean.add_list_headers(list)
@@ -69,6 +82,13 @@ module Mail
69
82
  clean.add_part new_part
70
83
  end
71
84
 
85
+ if self.protected_headers_subject.present?
86
+ new_part = Mail::Part.new
87
+ new_part.content_type = "text/rfc822-headers; protected-headers=v1"
88
+ new_part.body = "Subject: #{self.subject}\n"
89
+ clean.add_part new_part
90
+ end
91
+
72
92
  # Attach body or mime-parts in a new wrapper-part, to preserve the
73
93
  # original mime-structure.
74
94
  # We can't use self.to_s here — that includes all the headers we *don't*
@@ -203,15 +223,19 @@ module Mail
203
223
  end
204
224
 
205
225
  @keywords = []
226
+ look_for_keywords = true
206
227
  lines = part.decoded.lines.map do |line|
207
228
  # TODO: Find multiline arguments (add-key). Currently add-key has to
208
229
  # 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,}/)
230
+ if look_for_keywords && (m = line.match(/^x-([^:\s]*)[:\s]*(.*)/i))
231
+ command = m[1].strip.downcase
232
+ arguments = m[2].to_s.strip.downcase.split(/[,; ]{1,}/)
212
233
  @keywords << [command, arguments]
213
234
  nil
214
235
  else
236
+ if look_for_keywords && line.match(/\S+/i)
237
+ look_for_keywords = false
238
+ end
215
239
  line
216
240
  end
217
241
  end
@@ -259,17 +283,7 @@ module Mail
259
283
  @dynamic_pseudoheaders || []
260
284
  end
261
285
 
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
-
286
+ def signature_state
273
287
  # Careful to add information about the incoming signature. GPGME
274
288
  # throws exceptions if it doesn't know the key.
275
289
  if self.signature.present?
@@ -277,22 +291,41 @@ module Mail
277
291
  # for that manually and provide our own fallback. (Calling
278
292
  # `signature.key` results in an EOFError in that case.)
279
293
  if signing_key.present?
280
- msg = signature.to_s
294
+ signature_state = signature.to_s
281
295
  else
282
- # TODO: I18n
283
- msg = "Unknown signature by unknown key 0x#{self.signature.fingerprint}"
296
+ signature_state = I18n.t("signature_states.unknown", fingerprint: self.signature.fingerprint)
284
297
  end
285
298
  else
286
- # TODO: I18n
287
- msg = "Unsigned"
299
+ signature_state = I18n.t("signature_states.unsigned")
300
+ end
301
+ signature_state
302
+ end
303
+
304
+ def encryption_state
305
+ if was_encrypted?
306
+ encryption_state = I18n.t("encryption_states.encrypted")
307
+ else
308
+ encryption_state = I18n.t("encryption_states.unencrypted")
309
+ end
310
+ encryption_state
311
+ end
312
+
313
+ def standard_pseudoheaders(list)
314
+ if @standard_pseudoheaders.present?
315
+ return @standard_pseudoheaders
316
+ else
317
+ @standard_pseudoheaders = []
318
+ end
319
+
320
+ Array(list.headers_to_meta).each do |field|
321
+ value = case field.to_s
322
+ when 'sig' then signature_state
323
+ when 'enc' then encryption_state
324
+ else self.header[field.to_s]
325
+ end
326
+ @standard_pseudoheaders << make_pseudoheader(field.to_s, value)
288
327
  end
289
- @standard_pseudoheaders << make_pseudoheader(:sig, msg)
290
328
 
291
- # TODO: I18n
292
- @standard_pseudoheaders << make_pseudoheader(
293
- :enc,
294
- was_encrypted? ? 'Encrypted' : 'Unencrypted'
295
- )
296
329
 
297
330
  @standard_pseudoheaders
298
331
  end
@@ -316,7 +349,7 @@ module Mail
316
349
  if list.include_list_headers
317
350
  self['List-Id'] = "<#{list.email.gsub('@', '.')}>"
318
351
  self['List-Owner'] = "<mailto:#{list.owner_address}> (Use list's public key)"
319
- self['List-Help'] = '<https://schleuder.nadir.org/>'
352
+ self['List-Help'] = '<https://schleuder.org/>'
320
353
 
321
354
  postmsg = if list.receive_admin_only
322
355
  "NO (Admins only)"
@@ -410,6 +443,13 @@ module Mail
410
443
 
411
444
  private
412
445
 
446
+ # mail.signed? throws an error if it finds
447
+ # pgp boundaries, so we must use the Mail::Gpg
448
+ # methods.
449
+ def encapsulated_signed?(mail)
450
+ (mail.verify_result.nil? || mail.verify_result.signatures.empty?) && \
451
+ (Mail::Gpg.signed_mime?(mail) || Mail::Gpg.signed_inline?(mail))
452
+ end
413
453
 
414
454
  def add_footer!(footer_attribute)
415
455
  if self.list.blank? || self.list.send(footer_attribute).to_s.empty?
@@ -1,7 +1,6 @@
1
1
  module Schleuder
2
2
  module RequestPlugins
3
3
  def self.add_key(arguments, list, mail)
4
- out = [I18n.t('plugins.key_management.import_result')]
5
4
 
6
5
  if mail.has_attachments?
7
6
  results = self.import_keys_from_attachments(list, mail)
@@ -9,15 +8,35 @@ module Schleuder
9
8
  results = [self.import_key_from_body(list, mail)]
10
9
  end
11
10
 
12
- out << results.compact.collect(&:imports).flatten.map do |import_status|
13
- str = I18n.t("plugins.key_management.key_import_status.#{import_status.action}")
14
- "#{import_status.fpr}: #{str}"
11
+ import_stati = results.compact.collect(&:imports).flatten
12
+
13
+ if import_stati.blank?
14
+ return I18n.t('plugins.key_management.no_imports')
15
15
  end
16
16
 
17
- out.join("\n")
17
+ out = []
18
+
19
+ import_stati.each do |import_status|
20
+ if import_status.action == 'error'
21
+ out << I18n.t("plugins.key_management.key_import_status.error", fingerprint: import_status.fingerprint)
22
+ else
23
+ key = list.gpg.find_distinct_key(import_status.fingerprint)
24
+ if key
25
+ out << I18n.t("plugins.key_management.key_import_status.#{import_status.action}", key_oneline: key.oneline)
26
+ end
27
+ end
28
+ end
29
+
30
+ out.join("\n\n")
18
31
  end
19
32
 
20
33
  def self.delete_key(arguments, list, mail)
34
+ if arguments.blank?
35
+ return I18n.t(
36
+ "plugins.key_management.delete_key_requires_arguments"
37
+ )
38
+ end
39
+
21
40
  arguments.map do |argument|
22
41
  keys = list.keys(argument)
23
42
  case keys.size
@@ -26,9 +45,9 @@ module Schleuder
26
45
  when 1
27
46
  begin
28
47
  keys.first.delete!
29
- I18n.t('plugins.key_management.deleted', key_string: keys.first.fingerprint)
48
+ I18n.t('plugins.key_management.deleted', key_string: keys.first.oneline)
30
49
  rescue GPGME::Error::Conflict
31
- I18n.t('plugins.key_management.not_deletable', key_string: keys.first.fingerprint)
50
+ I18n.t('plugins.key_management.not_deletable', key_string: keys.first.oneline)
32
51
  end
33
52
  else
34
53
  I18n.t('errors.too_many_matching_keys', {
@@ -71,6 +90,12 @@ module Schleuder
71
90
  end
72
91
 
73
92
  def self.fetch_key(arguments, list, mail)
93
+ if arguments.blank?
94
+ return I18n.t(
95
+ "plugins.key_management.fetch_key_requires_arguments"
96
+ )
97
+ end
98
+
74
99
  arguments.map do |argument|
75
100
  list.fetch_keys(argument)
76
101
  end
@@ -1,12 +1,18 @@
1
1
  module Schleuder
2
2
  module RequestPlugins
3
3
  def self.subscribe(arguments, list, mail)
4
+ if arguments.blank?
5
+ return I18n.t(
6
+ "plugins.subscription_management.subscribe_requires_arguments"
7
+ )
8
+ end
9
+
4
10
  email = arguments.shift
5
11
 
6
12
  if arguments.present?
7
13
  # Collect all arguments that look like fingerprint-material
8
14
  fingerprint = ''
9
- while arguments.first.present? && arguments.first.match(/\A(0x)?[a-f0-9]+/i)
15
+ while arguments.first.present? && arguments.first.match(/^(0x)?[a-f0-9]+$/i)
10
16
  fingerprint << arguments.shift
11
17
  end
12
18
  # Use possibly remaining args as flags.
@@ -37,6 +43,13 @@ module Schleuder
37
43
  # If no address was given we unsubscribe the sender.
38
44
  email = arguments.first.presence || mail.signer.email
39
45
 
46
+ # Refuse to unsubscribe the last admin.
47
+ if list.admins.size == 1 && list.admins.first.email == email
48
+ return I18n.t(
49
+ "plugins.subscription_management.cannot_unsubscribe_last_admin", email: email
50
+ )
51
+ end
52
+
40
53
  # TODO: May signers have multiple UIDs? We don't match those currently.
41
54
  if ! list.from_admin?(mail) && email != mail.signer.email
42
55
  # Only admins may unsubscribe others.
@@ -98,6 +111,12 @@ module Schleuder
98
111
  end
99
112
 
100
113
  def self.set_fingerprint(arguments, list, mail)
114
+ if arguments.blank?
115
+ return I18n.t(
116
+ "plugins.subscription_management.set_fingerprint_requires_arguments"
117
+ )
118
+ end
119
+
101
120
  if arguments.first.match(/@/)
102
121
  if arguments.first == mail.signer.email || list.from_admin?(mail)
103
122
  email = arguments.shift
@@ -118,8 +137,15 @@ module Schleuder
118
137
  )
119
138
  end
120
139
 
121
- sub.fingerprint = arguments.join
140
+ fingerprint = arguments.join
141
+ unless GPGME::Key.valid_fingerprint?(fingerprint)
142
+ return I18n.t(
143
+ "plugins.subscription_management.set_fingerprint_requires_valid_fingerprint",
144
+ fingerprint: fingerprint
145
+ )
146
+ end
122
147
 
148
+ sub.fingerprint = fingerprint
123
149
  if sub.save
124
150
  I18n.t(
125
151
  "plugins.subscription_management.fingerprint_set",
@@ -130,7 +156,48 @@ module Schleuder
130
156
  I18n.t(
131
157
  "plugins.subscription_management.setting_fingerprint_failed",
132
158
  email: email,
133
- fingerprint: arguments.last,
159
+ fingerprint: sub.fingerprint,
160
+ errors: sub.errors.to_a.join("\n")
161
+ )
162
+ end
163
+ end
164
+
165
+ def self.unset_fingerprint(arguments, list, mail)
166
+ if arguments.blank?
167
+ return I18n.t(
168
+ "plugins.subscription_management.unset_fingerprint_requires_arguments"
169
+ )
170
+ end
171
+
172
+ email = arguments.first
173
+ unless email == mail.signer.email || list.from_admin?(mail)
174
+ return I18n.t(
175
+ "plugins.subscription_management.unset_fingerprint_only_self"
176
+ )
177
+ end
178
+ if email == mail.signer.email && list.from_admin?(mail) && arguments.last != 'force'
179
+ return I18n.t(
180
+ "plugins.subscription_management.unset_fingerprint_requires_arguments"
181
+ )
182
+ end
183
+
184
+ sub = list.subscriptions.where(email: email).first
185
+ if sub.blank?
186
+ return I18n.t(
187
+ "plugins.subscription_management.is_not_subscribed", email: email
188
+ )
189
+ end
190
+
191
+ sub.fingerprint = ''
192
+ if sub.save
193
+ I18n.t(
194
+ "plugins.subscription_management.fingerprint_unset",
195
+ email: email
196
+ )
197
+ else
198
+ I18n.t(
199
+ "plugins.subscription_management.unsetting_fingerprint_failed",
200
+ email: email,
134
201
  errors: sub.errors.to_a.join("\n")
135
202
  )
136
203
  end