schleuder 3.5.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -10
  3. data/Rakefile +12 -12
  4. data/bin/schleuder +1 -1
  5. data/db/migrate/20140501103532_create_lists.rb +1 -1
  6. data/db/migrate/20140501112859_create_subscriptions.rb +1 -1
  7. data/db/migrate/{201508092100_add_language_to_lists.rb → 20150809210000_add_language_to_lists.rb} +1 -1
  8. data/db/migrate/20150812165700_change_keywords_admin_only_defaults.rb +1 -1
  9. data/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb +1 -1
  10. data/db/migrate/{201508141727_change_send_encrypted_only_default.rb → 20150814172700_change_send_encrypted_only_default.rb} +1 -1
  11. data/db/migrate/{201508222143_add_logfiles_to_keep_to_lists.rb → 20150822214300_add_logfiles_to_keep_to_lists.rb} +1 -1
  12. data/db/migrate/{201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb → 20150826172300_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb} +1 -1
  13. data/db/migrate/{201508261815_strip_gpg_passphrase.rb → 20150826181500_strip_gpg_passphrase.rb} +1 -1
  14. data/db/migrate/{201508261827_remove_default_mime.rb → 20150826182700_remove_default_mime.rb} +1 -1
  15. data/db/migrate/20160501172700_fix_headers_to_meta_defaults.rb +1 -1
  16. data/db/migrate/20170713215059_add_internal_footer_to_list.rb +1 -1
  17. data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +1 -1
  18. data/db/migrate/20180723173900_add_deliver_selfsent_to_list.rb +1 -1
  19. data/db/migrate/20190906194820_add_autocrypt_header_to_list.rb +1 -1
  20. data/db/migrate/20200118170110_add_set_reply_to_to_sender_and_munge_from.rb +15 -0
  21. data/db/schema.rb +45 -45
  22. data/etc/list-defaults.yml +18 -0
  23. data/etc/postfix/schleuder_sqlite.cf +1 -1
  24. data/etc/schleuder-weekly-key-maintenance.service +9 -0
  25. data/etc/schleuder-weekly-key-maintenance.timer +9 -0
  26. data/etc/schleuder.yml +3 -3
  27. data/lib/schleuder-api-daemon/helpers/schleuder-api-daemon-helper.rb +3 -3
  28. data/lib/schleuder-api-daemon/routes/subscription.rb +4 -4
  29. data/lib/schleuder.rb +10 -12
  30. data/lib/schleuder/cli.rb +9 -188
  31. data/lib/schleuder/cli/cert.rb +2 -2
  32. data/lib/schleuder/cli/cli_helper.rb +14 -0
  33. data/lib/schleuder/cli/schleuder_cert_manager.rb +4 -4
  34. data/lib/schleuder/conf.rb +10 -4
  35. data/lib/schleuder/errors/base.rb +2 -2
  36. data/lib/schleuder/errors/decryption_failed.rb +1 -1
  37. data/lib/schleuder/errors/fatal_error.rb +1 -1
  38. data/lib/schleuder/errors/key_adduid_failed.rb +1 -1
  39. data/lib/schleuder/errors/key_generation_failed.rb +1 -1
  40. data/lib/schleuder/errors/message_empty.rb +1 -1
  41. data/lib/schleuder/errors/message_too_big.rb +1 -1
  42. data/lib/schleuder/errors/too_many_keys.rb +1 -1
  43. data/lib/schleuder/filters/post_decryption/10_request.rb +3 -3
  44. data/lib/schleuder/filters/post_decryption/20_max_message_size.rb +1 -1
  45. data/lib/schleuder/filters/post_decryption/30_forward_to_owner.rb +1 -1
  46. data/lib/schleuder/filters/post_decryption/40_receive_admin_only.rb +1 -1
  47. data/lib/schleuder/filters/post_decryption/50_receive_authenticated_only.rb +1 -1
  48. data/lib/schleuder/filters/post_decryption/60_receive_signed_only.rb +1 -1
  49. data/lib/schleuder/filters/post_decryption/70_receive_encrypted_only.rb +1 -1
  50. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +1 -1
  51. data/lib/schleuder/filters/pre_decryption/10_forward_bounce_to_admins.rb +1 -1
  52. data/lib/schleuder/filters/pre_decryption/30_send_key.rb +1 -1
  53. data/lib/schleuder/filters/pre_decryption/40_fix_exchange_messages.rb +1 -1
  54. data/lib/schleuder/filters/pre_decryption/50_strip_html_from_alternative.rb +2 -2
  55. data/lib/schleuder/filters_runner.rb +9 -9
  56. data/lib/schleuder/gpgme/ctx.rb +15 -35
  57. data/lib/schleuder/gpgme/key.rb +4 -136
  58. data/lib/schleuder/gpgme/user_id.rb +2 -0
  59. data/lib/schleuder/keyword_handlers/attach_list_key.rb +17 -0
  60. data/lib/schleuder/keyword_handlers/base.rb +36 -0
  61. data/lib/schleuder/keyword_handlers/get_version.rb +11 -0
  62. data/lib/schleuder/keyword_handlers/key_management.rb +141 -0
  63. data/lib/schleuder/keyword_handlers/list_management.rb +19 -0
  64. data/lib/schleuder/keyword_handlers/resend.rb +208 -0
  65. data/lib/schleuder/keyword_handlers/sign_this.rb +54 -0
  66. data/lib/schleuder/keyword_handlers/subscription_management.rb +213 -0
  67. data/lib/schleuder/keyword_handlers_runner.rb +146 -0
  68. data/lib/schleuder/list.rb +28 -40
  69. data/lib/schleuder/list_builder.rb +16 -5
  70. data/lib/schleuder/listlogger.rb +1 -1
  71. data/lib/schleuder/mail/message.rb +135 -40
  72. data/lib/schleuder/runner.rb +18 -16
  73. data/lib/schleuder/subscription.rb +35 -13
  74. data/lib/schleuder/validators/boolean_validator.rb +1 -1
  75. data/lib/schleuder/validators/email_validator.rb +1 -1
  76. data/lib/schleuder/validators/fingerprint_validator.rb +1 -1
  77. data/lib/schleuder/validators/greater_than_zero_validator.rb +1 -1
  78. data/lib/schleuder/validators/no_line_breaks_validator.rb +1 -1
  79. data/lib/schleuder/version.rb +1 -1
  80. data/locales/de.yml +49 -36
  81. data/locales/en.yml +34 -21
  82. metadata +119 -54
  83. data/bin/pinentry-clearpassphrase +0 -72
  84. data/lib/schleuder/plugin_runners/base.rb +0 -91
  85. data/lib/schleuder/plugin_runners/list_plugins_runner.rb +0 -24
  86. data/lib/schleuder/plugin_runners/request_plugins_runner.rb +0 -27
  87. data/lib/schleuder/plugins/attach_listkey.rb +0 -13
  88. data/lib/schleuder/plugins/get_version.rb +0 -7
  89. data/lib/schleuder/plugins/key_management.rb +0 -138
  90. data/lib/schleuder/plugins/list_management.rb +0 -15
  91. data/lib/schleuder/plugins/resend.rb +0 -199
  92. data/lib/schleuder/plugins/sign_this.rb +0 -46
  93. data/lib/schleuder/plugins/subscription_management.rb +0 -207
@@ -20,7 +20,7 @@ module Schleuder
20
20
  end
21
21
 
22
22
  def run
23
- Schleuder.logger.info "Building new list"
23
+ Schleuder.logger.info 'Building new list'
24
24
 
25
25
  if @listname.blank? || ! @listname.match(Conf::EMAIL_REGEXP)
26
26
  return [nil, {'email' => ["'#{@listname}' is not a valid email address"]}]
@@ -63,13 +63,13 @@ module Schleuder
63
63
 
64
64
  def gpg
65
65
  @gpg_ctx ||= begin
66
- ENV["GNUPGHOME"] = @list_dir
66
+ ENV['GNUPGHOME'] = @list_dir
67
67
  GPGME::Ctx.new
68
68
  end
69
69
  end
70
70
 
71
71
  def create_key(list)
72
- Schleuder.logger.info "Generating key-pair, this could take a while..."
72
+ Schleuder.logger.info 'Generating key-pair, this could take a while...'
73
73
  gpg.generate_key(key_params(list))
74
74
 
75
75
  # Get key without knowing the fingerprint yet.
@@ -88,14 +88,14 @@ module Schleuder
88
88
  def adduids(list, key)
89
89
  # Add UIDs for -owner and -request.
90
90
  [list.request_address, list.owner_address].each do |address|
91
- err = key.adduid(list.email, address)
91
+ err = add_uid_to_key(list, address)
92
92
  if err.present?
93
93
  raise err
94
94
  end
95
95
  end
96
96
  # Go through list.key() to re-fetch the key from the keyring, otherwise
97
97
  # we don't see the new UIDs.
98
- errors = list.key.set_primary_uid(list.email)
98
+ errors = set_primary_uid_of_key(list)
99
99
  if errors.present?
100
100
  raise errors
101
101
  end
@@ -135,5 +135,16 @@ module Schleuder
135
135
  end
136
136
  end
137
137
 
138
+ def set_primary_uid_of_key(list)
139
+ errors, _ = GPGME::Ctx.gpgcli("--quick-set-primary-uid #{list.email} '#{list.email} <#{list.email}>'")
140
+ errors.join
141
+ end
142
+
143
+ def add_uid_to_key(list, email)
144
+ # Specifying the key via fingerprint apparently doesn't work.
145
+ errors, _ = GPGME::Ctx.gpgcli("--quick-adduid #{list.email} '#{list.email} <#{email}>'")
146
+ errors.join
147
+ end
148
+
138
149
  end
139
150
  end
@@ -17,7 +17,7 @@ module Schleuder
17
17
  if logfiles_to_keep < 1
18
18
  logfiles_to_keep = list.class.column_defaults['logfiles_to_keep']
19
19
  end
20
- suffix_now = Time.now.strftime("%Y%m%d").to_i
20
+ suffix_now = Time.now.strftime('%Y%m%d').to_i
21
21
  del_older_than = suffix_now - logfiles_to_keep
22
22
  Pathname.glob("#{list.logfile}.????????").each do |file|
23
23
  if file.basename.to_s.match(/\.([0-9]{8})$/)
@@ -24,7 +24,9 @@ module Mail
24
24
  # Message#initialize.
25
25
  def setup
26
26
  if self.encrypted?
27
- new = self.decrypt(verify: true)
27
+ # Specify 'loopback'-pinentry-mode to ensure that gnupg never-ever
28
+ # tries to interactively ask for a passphrase.
29
+ new = self.decrypt(verify: true, pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK)
28
30
  # Test if there's a signed multipart inside the ciphertext
29
31
  # ("encapsulated" format of pgp/mime).
30
32
  if encapsulated_signed?(new)
@@ -55,7 +57,7 @@ module Mail
55
57
  end
56
58
 
57
59
  # Delete the protected headers which might leak information.
58
- if new.parts.first && new.parts.first.content_type == "text/rfc822-headers; protected-headers=v1"
60
+ if new.parts.first && new.parts.first.content_type == 'text/rfc822-headers; protected-headers=v1'
59
61
  new.parts.shift
60
62
  end
61
63
 
@@ -82,7 +84,7 @@ module Mail
82
84
 
83
85
  if self.protected_headers_subject.present?
84
86
  new_part = Mail::Part.new
85
- new_part.content_type = "text/rfc822-headers; protected-headers=v1"
87
+ new_part.content_type = 'text/rfc822-headers; protected-headers=v1'
86
88
  new_part.body = "Subject: #{self.subject}\n"
87
89
  clean.add_part new_part
88
90
  end
@@ -141,7 +143,7 @@ module Mail
141
143
  when 1
142
144
  signatures.first
143
145
  else
144
- raise "Multiple signatures found! Cannot handle!"
146
+ raise 'Multiple signatures found! Cannot handle!'
145
147
  end
146
148
  end
147
149
 
@@ -161,8 +163,10 @@ module Mail
161
163
  # subscription-assigned fingerprints are (should be) the ones of the
162
164
  # primary keys, so we need to look up the key.
163
165
  def signing_key
164
- if signature.present?
165
- @signing_key ||= list.keys(signature.fpr).first
166
+ @signing_key ||= begin
167
+ if signature.present?
168
+ list.keys(signature.fpr).first
169
+ end
166
170
  end
167
171
  end
168
172
 
@@ -205,17 +209,17 @@ module Mail
205
209
  @recipient.match(/-bounce@/).present? ||
206
210
  # Empty Return-Path
207
211
  self.return_path.to_s == '<>' ||
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.
212
- ( self['Auto-Submitted'].present? && \
213
- self['Auto-Submitted'].to_s.downcase != 'no' && \
214
- !self['X-Cron-Env'].present? && \
215
- !self['X-Jenkins-Job'].present? && \
216
- self.subject.to_s !~ /\A\*\*\* SECURITY information.*\*\*\*\Z/)
212
+ bounced?
213
+ end
214
+
215
+ def bounced?
216
+ @bounced ||= bounce_detected? || (error_status != 'unknown')
217
217
  end
218
218
 
219
+ def error_status
220
+ @error_status ||= detect_error_code
221
+ end
222
+
219
223
  def keywords
220
224
  return @keywords if @keywords
221
225
 
@@ -224,23 +228,8 @@ module Mail
224
228
  return []
225
229
  end
226
230
 
227
- @keywords = []
228
- look_for_keywords = true
229
- lines = part.decoded.lines.map do |line|
230
- # TODO: Find multiline arguments (add-key). Currently add-key has to
231
- # read the whole body and hope for the best.
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,}/)
235
- @keywords << [command, arguments]
236
- nil
237
- else
238
- if look_for_keywords && line.match(/\S+/i)
239
- look_for_keywords = false
240
- end
241
- line
242
- end
243
- end
231
+ @keywords, lines = extract_keywords(part.decoded.lines)
232
+ new_body = lines.join
244
233
 
245
234
  # Work around problems with re-encoding the body. If we delete the
246
235
  # content-transfer-encoding prior to re-assigning the body, and let Mail
@@ -249,7 +238,6 @@ module Mail
249
238
  part.content_transfer_encoding = nil
250
239
 
251
240
  # Set the right charset on the now parsed body
252
- new_body = lines.compact.join
253
241
  part.charset = new_body.encoding.to_s
254
242
  part.body = new_body
255
243
 
@@ -292,19 +280,19 @@ module Mail
292
280
  if signing_key.present?
293
281
  signature_state = signature.to_s
294
282
  else
295
- signature_state = I18n.t("signature_states.unknown", fingerprint: self.signature.fingerprint)
283
+ signature_state = I18n.t('signature_states.unknown', fingerprint: self.signature.fingerprint)
296
284
  end
297
285
  else
298
- signature_state = I18n.t("signature_states.unsigned")
286
+ signature_state = I18n.t('signature_states.unsigned')
299
287
  end
300
288
  signature_state
301
289
  end
302
290
 
303
291
  def encryption_state
304
292
  if was_encrypted?
305
- encryption_state = I18n.t("encryption_states.encrypted")
293
+ encryption_state = I18n.t('encryption_states.encrypted')
306
294
  else
307
- encryption_state = I18n.t("encryption_states.unencrypted")
295
+ encryption_state = I18n.t('encryption_states.unencrypted')
308
296
  end
309
297
  encryption_state
310
298
  end
@@ -360,7 +348,7 @@ module Mail
360
348
  self['List-Help'] = '<https://schleuder.org/>'
361
349
 
362
350
  postmsg = if list.receive_admin_only
363
- "NO (Admins only)"
351
+ 'NO (Admins only)'
364
352
  elsif list.receive_authenticated_only
365
353
  "<mailto:#{list.email}> (Subscribers only)"
366
354
  else
@@ -437,7 +425,6 @@ module Mail
437
425
  end
438
426
  end
439
427
 
440
-
441
428
  def attach_list_key!(list)
442
429
  filename = "#{list.email}.asc"
443
430
  self.add_file({
@@ -451,6 +438,40 @@ module Mail
451
438
 
452
439
  private
453
440
 
441
+
442
+ def extract_keywords(content_lines)
443
+ keywords = []
444
+ in_keyword_block = false
445
+ found_blank_line = false
446
+ content_lines.each_with_index do |line, i|
447
+ if match = line.match(/^x-([^:\s]*)[:\s]*(.*)/i)
448
+ keyword = match[1].strip.downcase
449
+ arguments = match[2].to_s.strip.downcase.split(/[,; ]{1,}/)
450
+ keywords << [keyword, arguments]
451
+ in_keyword_block = true
452
+
453
+ # Set this line to nil to have it stripped from the message.
454
+ 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
+ elsif in_keyword_block == true
464
+ # Interpret line as arguments to the previous keyword.
465
+ keywords[-1][-1] += line.downcase.strip.split(/[,; ]{1,}/)
466
+ 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.
469
+ break
470
+ end
471
+ end
472
+ [keywords, content_lines.compact]
473
+ end
474
+
454
475
  # mail.signed? throws an error if it finds
455
476
  # pgp boundaries, so we must use the Mail::Gpg
456
477
  # methods.
@@ -480,7 +501,7 @@ module Mail
480
501
  end
481
502
 
482
503
  def _add_subject_prefix(suffix)
483
- attrib = "subject_prefix"
504
+ attrib = 'subject_prefix'
484
505
  if suffix
485
506
  attrib << "_#{suffix}"
486
507
  end
@@ -526,5 +547,79 @@ module Mail
526
547
  end
527
548
  end.join(' ')
528
549
  end
550
+
551
+ def detect_error_code
552
+ # Detects the error code of an email with different heuristics
553
+ # from: https://github.com/mailtop/bounce_email
554
+
555
+ # Custom status codes
556
+ unicode_subject = self.subject.to_s
557
+ unicode_subject = unicode_subject.encode('utf-8') if unicode_subject.respond_to?(:encode)
558
+
559
+ return '97' if unicode_subject.match(/delayed/i)
560
+ return '98' if unicode_subject.match(/(unzulässiger|unerlaubter) anhang/i)
561
+ return '99' if unicode_subject.match(/auto.*reply|férias|ferias|Estarei ausente|estou ausente|vacation|vocation|(out|away).*office|on holiday|abwesenheits|autorespond|Automatische|eingangsbestätigung/i)
562
+
563
+ # Feedback-Type: abuse
564
+ return '96' if self.to_s.match(/Feedback-Type: abuse/i)
565
+
566
+ if self.parts[1]
567
+ match_parts = self.parts[1].body.match(/(Status:.|550 |#)([245]\.[0-9]{1,3}\.[0-9]{1,3})/)
568
+ code = match_parts[2] if match_parts
569
+ return code if code
570
+ end
571
+
572
+ # Now try getting it from correct part of tmail
573
+ code = detect_bounce_status_code_from_text(self.body)
574
+ return code if code
575
+
576
+ # OK getting desperate so try getting code from entire email
577
+ code = detect_bounce_status_code_from_text(self.to_s)
578
+ code || 'unknown'
579
+ end
580
+
581
+ def bounce_detected?
582
+ # Detects bounces from different parts of the email without error status codes
583
+ # from: https://github.com/mailtop/bounce_email
584
+ return true if self.subject.to_s.match(/(returned|undelivered) mail|mail delivery( failed)?|(delivery )(status notification|failure)|failure notice|undeliver(able|ed)( mail)?|return(ing message|ed) to sender/i)
585
+ return true if self.subject.to_s.match(/auto.*reply|vacation|vocation|(out|away).*office|on holiday|abwesenheits|autorespond|Automatische|eingangsbestätigung/i)
586
+ return true if self['precedence'].to_s.match(/auto.*(reply|responder|antwort)/i)
587
+ return true if self.from.to_s.match(/^(MAILER-DAEMON|POSTMASTER)@/i)
588
+ false
589
+ end
590
+
591
+ def detect_bounce_status_code_from_text(text)
592
+ # Parses a text and uses pattern matching to determines its error status (RFC 3463)
593
+ # from: https://github.com/mailtop/bounce_email
594
+ return '5.0.0' if text.match(/Status: 5\.0\.0/i)
595
+ return '5.1.1' if text.match(/no such (address|user)|Recipient address rejected|User unknown|does not like recipient|The recipient was unavailable to take delivery of the message|Sorry, no mailbox here by that name|invalid address|unknown user|unknown local part|user not found|invalid recipient|failed after I sent the message|did not reach the following recipient|nicht zugestellt werden|o pode ser entregue para um ou mais/i)
596
+ return '5.1.2' if text.match(/unrouteable mail domain|Esta casilla ha expirado por falta de uso|I couldn't find any host named/i)
597
+ if text.match(/mailbox is full|Mailbox quota (usage|disk) exceeded|quota exceeded|Over quota|User mailbox exceeds allowed size|Message rejected\. Not enough storage space|user has exhausted allowed storage space|too many messages on the server|mailbox is over quota|mailbox exceeds allowed size|excedeu a quota/i)
598
+ return '5.2.2' if text.match(/This is a permanent error||(Status: |)5\.2\.2/i)
599
+ return '4.2.2'
600
+ end
601
+ return '5.1.0' if text.match(/Address rejected/)
602
+ return '4.1.2' if text.match(/I couldn't find any host by that name/)
603
+ return '4.2.0' if text.match(/not yet been delivered/i)
604
+ return '5.1.1' if text.match(/mailbox unavailable|No such mailbox|RecipientNotFound|not found by SMTP address lookup|Status: 5\.1\.1/i)
605
+ return '5.2.3' if text.match(/Status: 5\.2\.3/i) # Too messages in folder
606
+ return '5.4.0' if text.match(/Status: 5\.4\.0/i) # too many hops
607
+ return '5.4.4' if text.match(/Unrouteable address/i)
608
+ return '4.4.7' if text.match(/retry timeout exceeded/i)
609
+ return '5.2.0' if text.match(/The account or domain may not exist, they may be blacklisted, or missing the proper dns entries./i)
610
+ return '5.5.4' if text.match(/554 TRANSACTION FAILED/i)
611
+ return '4.4.1' if text.match(/Status: 4.4.1|delivery temporarily suspended|wasn't able to establish an SMTP connection/i)
612
+ return '5.5.0' if text.match(/550 OU-002|Mail rejected by Windows Live Hotmail for policy reasons/i)
613
+ return '5.1.2' if text.match(/PERM_FAILURE: DNS Error: Domain name not found/i)
614
+ return '4.2.0' if text.match(/Delivery attempts will continue to be made for/i)
615
+ return '5.5.4' if text.match(/554 delivery error:/i)
616
+ return '5.1.1' if text.match(/550-5.1.1|This Gmail user does not exist/i)
617
+ return '5.7.1' if text.match(/5.7.1 Your message.*?was blocked by ROTA DNSBL/i) # AA added
618
+ return '5.7.2' if text.match(/not have permission to post messages to the group/i)
619
+ return '5.3.2' if text.match(/Technical details of permanent failure|Too many bad recipients/i) && (text.match(/The recipient server did not accept our requests to connect/i) || text.match(/Connection was dropped by remote host/i) || text.match(/Could not initiate SMTP conversation/i)) # AA added
620
+ return '4.3.2' if text.match(/Technical details of temporary failure/i) && (text.match(/The recipient server did not accept our requests to connect/i) || text.match(/Connection was dropped by remote host/i) || text.match(/Could not initiate SMTP conversation/i)) # AA added
621
+ return '5.0.0' if text.match(/Delivery to the following recipient failed permanently/i) # AA added
622
+ return '5.2.3' if text.match(/account closed|account has been disabled or discontinued|mailbox not found|prohibited by administrator|access denied|account does not exist/i)
623
+ end
529
624
  end
530
625
  end
@@ -4,12 +4,12 @@ module Schleuder
4
4
  error = setup_list(recipient)
5
5
  return error if error
6
6
 
7
- logger.info "Parsing incoming email."
7
+ logger.info 'Parsing incoming email.'
8
8
 
9
9
  # is it valid utf-8?
10
10
  msg_scrubbed = false
11
11
  unless msg.valid_encoding?
12
- logger.warn "Converting message due to invalid characters"
12
+ logger.warn 'Converting message due to invalid characters'
13
13
  detection = CharlockHolmes::EncodingDetector.detect(msg)
14
14
  begin
15
15
  msg = CharlockHolmes::Converter.convert(msg, detection[:encoding], 'UTF-8')
@@ -18,7 +18,7 @@ module Schleuder
18
18
  # so we scrub the invalid characters to be able to
19
19
  # at least parse the message somehow. Though this might
20
20
  # result in data loss.
21
- logger.warn "Scrubbing message due to invalid characters"
21
+ logger.warn 'Scrubbing message due to invalid characters'
22
22
  msg = msg.scrub
23
23
  msg_scrubbed = true
24
24
  end
@@ -27,7 +27,7 @@ module Schleuder
27
27
  @mail = Mail.create_message_to_list(msg, recipient, list)
28
28
 
29
29
  if msg_scrubbed
30
- @mail.add_pseudoheader(:note, I18n.t("pseudoheaders.scrubbed_message"))
30
+ @mail.add_pseudoheader(:note, I18n.t('pseudoheaders.scrubbed_message'))
31
31
  end
32
32
 
33
33
  error = run_filters('pre')
@@ -40,9 +40,10 @@ module Schleuder
40
40
  rescue GPGME::Error::BadPassphrase,
41
41
  GPGME::Error::DecryptFailed,
42
42
  GPGME::Error::NoData,
43
- GPGME::Error::NoSecretKey
43
+ GPGME::Error::NoSecretKey,
44
+ GPGME::Error::Canceled
44
45
 
45
- logger.warn "Decryption of incoming message failed."
46
+ logger.warn 'Decryption of incoming message failed.'
46
47
  return Errors::DecryptionFailed.new(list)
47
48
  end
48
49
 
@@ -50,29 +51,29 @@ module Schleuder
50
51
  return error if error
51
52
 
52
53
  if ! @mail.was_validly_signed?
53
- logger.debug "Message was not validly signed, adding subject_prefix_in"
54
+ logger.debug 'Message was not validly signed, adding subject_prefix_in'
54
55
  @mail.add_subject_prefix_in!
55
56
  end
56
57
 
57
58
  if ! @mail.was_encrypted?
58
- logger.debug "Message was not encrypted, skipping plugins"
59
+ logger.debug 'Message was not encrypted, skipping keyword-handlers'
59
60
  elsif @mail.was_validly_signed?
60
- # Plugins
61
- logger.debug "Message was encrypted and validly signed"
62
- PluginRunners::ListPluginsRunner.run(list, @mail).compact
61
+ # run KeywordHandlers
62
+ logger.debug 'Message was encrypted and validly signed'
63
+ KeywordHandlersRunner.run(type: :list, list: list, mail: @mail).compact
63
64
  end
64
65
 
65
66
  # Don't send empty messages over the list.
66
67
  if @mail.empty?
67
- logger.info "Message found empty, not sending it to list."
68
+ logger.info 'Message found empty, not sending it to list.'
68
69
  return Errors::MessageEmpty.new(@list)
69
70
  end
70
71
 
71
- logger.debug "Adding subject_prefix"
72
+ logger.debug 'Adding subject_prefix'
72
73
  @mail.add_subject_prefix!
73
74
 
74
75
  # Subscriptions
75
- logger.debug "Creating clean copy of message"
76
+ logger.debug 'Creating clean copy of message'
76
77
  copy = @mail.clean_copy(list.headers_to_meta.any?)
77
78
  list.send_to_subscriptions(copy, @mail)
78
79
  nil
@@ -105,10 +106,11 @@ module Schleuder
105
106
  end
106
107
 
107
108
  def filters_runner_pre_decryption
108
- @filters_runner_pre_decryption ||= Filters::Runner.new(list,'pre')
109
+ @filters_runner_pre_decryption ||= Filters::Runner.new(list, 'pre')
109
110
  end
111
+
110
112
  def filters_runner_post_decryption
111
- @filters_runner_post_decryption ||= Filters::Runner.new(list,'post')
113
+ @filters_runner_post_decryption ||= Filters::Runner.new(list, 'post')
112
114
  end
113
115
 
114
116
  def logger
@@ -4,22 +4,26 @@ module Schleuder
4
4
 
5
5
  validates :list_id, inclusion: {
6
6
  in: -> (id) { List.pluck(:id) },
7
- message: "must refer to an existing list"
7
+ message: 'must refer to an existing list'
8
8
  }
9
- validates :email, presence: true, email: true, uniqueness: {scope: :list_id}
9
+ validates :email, presence: true, email: true
10
+ validates :email, uniqueness: { scope: :list_id, case_sensitive: true }
10
11
  validates :fingerprint, allow_blank: true, fingerprint: true
11
12
  validates :delivery_enabled, :admin, boolean: true
12
13
 
13
- default_scope { order(:email) }
14
+ before_validation {
15
+ self.email = Mail::Address.new(self.email).address
16
+ self.email.downcase! if self.email.present?
17
+ }
14
18
 
15
- scope :without_fingerprint, -> { where(fingerprint: [nil,'']) }
19
+ default_scope { order(:email) }
16
20
 
17
21
  def to_s
18
22
  email
19
23
  end
20
24
 
21
25
  def self.configurable_attributes
22
- [:fingerprint, :admin, :delivery_enabled]
26
+ ['fingerprint', 'admin', 'delivery_enabled']
23
27
  end
24
28
 
25
29
  def fingerprint=(arg)
@@ -34,13 +38,13 @@ module Schleuder
34
38
  # TODO: make key-related methods a concern, so we don't have to go
35
39
  # through the list and neither re-implement the methods here.
36
40
  # Prefix '0x' to force GnuPG to match only hex-values, not UIDs.
37
- list.keys("0x#{self.fingerprint}").first
41
+ @key ||= list.keys("0x#{self.fingerprint}").first
38
42
  end
39
43
 
40
- def send_mail(mail)
44
+ def send_mail(mail, incoming_mail=nil)
41
45
  list.logger.debug "Preparing sending to #{self.inspect}"
42
46
 
43
- mail = ensure_headers(mail)
47
+ mail = ensure_headers(mail, incoming_mail)
44
48
  gpg_opts = self.list.gpg_sign_options
45
49
 
46
50
  if self.key.blank?
@@ -48,7 +52,7 @@ module Schleuder
48
52
  notify_of_missed_message(:absent)
49
53
  return false
50
54
  else
51
- list.logger.warn "Sending plaintext because no key is present!"
55
+ list.logger.warn 'Sending plaintext because no key is present!'
52
56
  end
53
57
  elsif ! self.key.usable?
54
58
  if self.list.send_encrypted_only?
@@ -66,9 +70,28 @@ module Schleuder
66
70
  mail.deliver
67
71
  end
68
72
 
69
- def ensure_headers(mail)
73
+ def ensure_headers(mail, incoming_mail=nil)
70
74
  mail.to = self.email
71
- mail.from = self.list.email
75
+
76
+ if self.list.set_reply_to_to_sender? && ! incoming_mail.nil?
77
+ # If the option "set_reply_to_to_sender" is set to true, we will set the reply-to header
78
+ # to the reply-to header given by the original email. If no reply-to header exists in the original email,
79
+ # the original senders email will be used as reply-to.
80
+ if ! incoming_mail.reply_to.nil?
81
+ mail.reply_to = incoming_mail.reply_to
82
+ else
83
+ mail.reply_to = incoming_mail.from
84
+ end
85
+ end
86
+
87
+ if self.list.munge_from? && ! incoming_mail.nil?
88
+ # If the option "munge_from" is set to true, we will add the original senders' from-header to ours.
89
+ # We munge the from-header to avoid issues with DMARC.
90
+ mail.from = I18n.t('header_munging', from: incoming_mail.from.first, list: self.list.email, list_address: self.list.email)
91
+ else
92
+ mail.from = self.list.email
93
+ end
94
+
72
95
  mail.sender = self.list.bounce_address
73
96
  mail
74
97
  end
@@ -77,7 +100,7 @@ module Schleuder
77
100
  self.list.logger.warn "Not sending to #{self.email}: key is unusable because it is #{reason} and sending plain text not allowed"
78
101
  mail = ensure_headers(Mail.new)
79
102
  mail.subject = I18n.t('notice')
80
- mail.body = I18n.t("missed_message_due_to_unusable_key", list_email: self.list.email) + I18n.t('errors.signoff')
103
+ mail.body = I18n.t('missed_message_due_to_unusable_key', list_email: self.list.email) + I18n.t('errors.signoff')
81
104
  mail.gpg self.list.gpg_sign_options
82
105
  mail.deliver
83
106
  end
@@ -89,6 +112,5 @@ module Schleuder
89
112
  def delete_key
90
113
  list.delete_key(self.fingerprint)
91
114
  end
92
-
93
115
  end
94
116
  end