schleuder 3.5.3 → 4.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -21
  3. data/Rakefile +15 -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 +13 -12
  30. data/lib/schleuder/cli.rb +9 -189
  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 +4 -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 -67
  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/logger.rb +2 -6
  72. data/lib/schleuder/mail/{encrypted_part.rb → gpg/encrypted_part.rb} +0 -0
  73. data/lib/schleuder/mail/gpg/sign_part.rb +33 -0
  74. data/lib/schleuder/mail/message.rb +135 -40
  75. data/lib/schleuder/runner.rb +18 -16
  76. data/lib/schleuder/subscription.rb +35 -13
  77. data/lib/schleuder/validators/boolean_validator.rb +1 -1
  78. data/lib/schleuder/validators/email_validator.rb +1 -1
  79. data/lib/schleuder/validators/fingerprint_validator.rb +1 -1
  80. data/lib/schleuder/validators/greater_than_zero_validator.rb +1 -1
  81. data/lib/schleuder/validators/no_line_breaks_validator.rb +1 -1
  82. data/lib/schleuder/version.rb +1 -1
  83. data/locales/de.yml +49 -36
  84. data/locales/en.yml +34 -21
  85. metadata +131 -79
  86. data/bin/pinentry-clearpassphrase +0 -72
  87. data/lib/schleuder/plugin_runners/base.rb +0 -91
  88. data/lib/schleuder/plugin_runners/list_plugins_runner.rb +0 -24
  89. data/lib/schleuder/plugin_runners/request_plugins_runner.rb +0 -27
  90. data/lib/schleuder/plugins/attach_listkey.rb +0 -13
  91. data/lib/schleuder/plugins/get_version.rb +0 -7
  92. data/lib/schleuder/plugins/key_management.rb +0 -121
  93. data/lib/schleuder/plugins/list_management.rb +0 -15
  94. data/lib/schleuder/plugins/resend.rb +0 -199
  95. data/lib/schleuder/plugins/sign_this.rb +0 -46
  96. data/lib/schleuder/plugins/subscription_management.rb +0 -207
@@ -26,8 +26,8 @@ module GPGME
26
26
  expired.present?
27
27
  end
28
28
 
29
- def oneline
30
- @oneline ||=
29
+ def summary
30
+ @summary ||=
31
31
  begin
32
32
  datefmt = '%Y-%m-%d'
33
33
  attribs = [
@@ -41,7 +41,7 @@ module GPGME
41
41
  attribs << "[expired: #{expires.strftime(datefmt)}]"
42
42
  when :revoked
43
43
  # TODO: add revocation date when it's available.
44
- attribs << "[revoked]"
44
+ attribs << '[revoked]'
45
45
  else
46
46
  attribs << "[#{usability_issue}]"
47
47
  end
@@ -75,144 +75,12 @@ module GPGME
75
75
  if trust.present?
76
76
  trust
77
77
  elsif ! usable_for?(:encrypt)
78
- "not capable of encryption"
78
+ 'not capable of encryption'
79
79
  else
80
80
  nil
81
81
  end
82
82
  end
83
83
 
84
- def set_primary_uid(email)
85
- # We rely on the order of UIDs here. Seems to work.
86
- index = self.uids.map(&:email).index(email)
87
- uid_number = index + 1
88
- primary_set = false
89
- args = "--edit-key '#{self.fingerprint}' #{uid_number}"
90
- errors, _ = GPGME::Ctx.gpgcli_expect(args) do |line|
91
- case line.chomp
92
- when /keyedit.prompt/
93
- if ! primary_set
94
- primary_set = true
95
- "primary"
96
- else
97
- "save"
98
- end
99
- else
100
- nil
101
- end
102
- end
103
- errors.join
104
- end
105
-
106
- def adduid(uid, email)
107
- # This block can be deleted once we cease to support gnupg 2.0.
108
- if ! GPGME::Ctx.sufficient_gpg_version?('2.1.4')
109
- return adduid_expect(uid, email)
110
- end
111
-
112
- # Specifying the key via fingerprint apparently doesn't work.
113
- errors, _ = GPGME::Ctx.gpgcli("--quick-adduid #{uid} '#{uid} <#{email}>'")
114
- errors.join
115
- end
116
-
117
- # This method can be deleted once we cease to support gnupg 2.0.
118
- def adduid_expect(uid, email)
119
- args = "--allow-freeform-uid --edit-key '#{self.fingerprint}' adduid"
120
- errors, _ = GPGME::Ctx.gpgcli_expect(args) do |line|
121
- case line.chomp
122
- when /keygen.name/
123
- uid
124
- when /keygen.email/
125
- email
126
- when /keygen.comment/
127
- ''
128
- when /keyedit.prompt/
129
- "save"
130
- else
131
- nil
132
- end
133
- end
134
- errors.join
135
- end
136
-
137
- def clearpassphrase(oldpw)
138
- # This block can be deleted once we cease to support gnupg 2.0.
139
- if ! GPGME::Ctx.sufficient_gpg_version?('2.1.0')
140
- return clearpassphrase_v20(oldpw)
141
- end
142
-
143
- oldpw_given = false
144
- # Don't use '--passwd', it claims to fail (even though it factually doesn't).
145
- args = "--pinentry-mode loopback --edit-key '#{self.fingerprint}' passwd"
146
- errors, _, exitcode = GPGME::Ctx.gpgcli_expect(args) do |line|
147
- case line
148
- when /passphrase.enter/
149
- if ! oldpw_given
150
- oldpw_given = true
151
- oldpw
152
- else
153
- ""
154
- end
155
- when /BAD_PASSPHRASE/
156
- [false, 'bad passphrase']
157
- when /change_passwd.empty.okay/
158
- 'y'
159
- when /keyedit.prompt/
160
- "save"
161
- else
162
- nil
163
- end
164
- end
165
-
166
- # Only show errors if something apparently went wrong. Otherwise we might
167
- # leak useless strings from gpg and make the caller report errors even
168
- # though this method succeeded.
169
- if exitcode > 0
170
- errors.join
171
- else
172
- nil
173
- end
174
- end
175
-
176
- # This method can be deleted once we cease to support gnupg 2.0.
177
- def clearpassphrase_v20(oldpw)
178
- start_gpg_agent(oldpw)
179
- # Don't use '--passwd', it claims to fail (even though it factually doesn't).
180
- errors, _, exitcode = GPGME::Ctx.gpgcli_expect("--edit-key '#{self.fingerprint}' passwd") do |line|
181
- case line
182
- when /BAD_PASSPHRASE/
183
- [false, 'bad passphrase']
184
- when /change_passwd.empty.okay/
185
- 'y'
186
- when /keyedit.prompt/
187
- "save"
188
- else
189
- nil
190
- end
191
- end
192
- stop_gpg_agent
193
-
194
- # Only show errors if something apparently went wrong. Otherwise we might
195
- # leak useless strings from gpg and make the caller report errors even
196
- # though this method succeeded.
197
- if exitcode > 0
198
- errors.join
199
- else
200
- nil
201
- end
202
- end
203
-
204
- # This method can be deleted once we cease to support gnupg 2.0.
205
- def stop_gpg_agent
206
- # gpg-agent terminates itself if its socket goes away.
207
- GPGME::Ctx.delete_daemon_socket('gpg-agent')
208
- end
209
-
210
- def start_gpg_agent(oldpw)
211
- ENV['PINENTRY_USER_DATA'] = oldpw
212
- pinentry = File.join(ENV['SCHLEUDER_ROOT'], 'bin', 'pinentry-clearpassphrase')
213
- GPGME::Ctx.spawn_daemon('gpg-agent', "--use-standard-socket --pinentry-program #{pinentry}")
214
- end
215
-
216
84
  def self.valid_fingerprint?(fp)
217
85
  fp =~ Schleuder::Conf::FINGERPRINT_REGEXP
218
86
  end
@@ -3,9 +3,11 @@ module GPGME
3
3
  def name
4
4
  sanitize_encoding(@name)
5
5
  end
6
+
6
7
  def comment
7
8
  sanitize_encoding(@comment)
8
9
  end
10
+
9
11
  def uid
10
12
  sanitize_encoding(@uid)
11
13
  end
@@ -0,0 +1,17 @@
1
+ module Schleuder
2
+ module KeywordHandlers
3
+ class AttachListKey < Base
4
+ handles_list_keyword 'attach-listkey', with_method: :attach_list_key
5
+
6
+ def attach_list_key
7
+ new_part = Mail::Part.new
8
+ new_part.body = @list.export_key
9
+ new_part.content_type = 'application/pgp-keys'
10
+ new_part.content_description = "OpenPGP public key of #{@list.email}"
11
+ new_part.content_disposition = "attachment; filename=#{@list.fingerprint}.pgpkey"
12
+ @mail.add_part new_part
13
+ nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module Schleuder
2
+ module KeywordHandlers
3
+ class Base
4
+ class << self
5
+ def handles_request_keyword(keyword, with_method:, has_aliases: [])
6
+ KeywordHandlersRunner.register_keyword(
7
+ type: :request,
8
+ keyword: keyword,
9
+ handler_class: self,
10
+ handler_method: with_method,
11
+ aliases: has_aliases
12
+ )
13
+ end
14
+
15
+ def handles_list_keyword(keyword, with_method:, has_aliases: [])
16
+ KeywordHandlersRunner.register_keyword(
17
+ type: :list,
18
+ keyword: keyword,
19
+ handler_class: self,
20
+ handler_method: with_method,
21
+ aliases: has_aliases
22
+ )
23
+ end
24
+ end
25
+
26
+ attr_reader :arguments
27
+ attr_reader :mail
28
+
29
+ def initialize(mail:, arguments:)
30
+ @arguments = arguments
31
+ @mail = mail
32
+ @list = mail.list
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ module Schleuder
2
+ module KeywordHandlers
3
+ class GetVersion < Base
4
+ handles_request_keyword 'get-version', with_method: :get_version
5
+
6
+ def get_version
7
+ Schleuder::VERSION
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,141 @@
1
+ module Schleuder
2
+ module KeywordHandlers
3
+ class KeyManagement < Base
4
+ handles_request_keyword 'add-key', with_method: 'add_key'
5
+ handles_request_keyword 'delete-key', with_method: 'delete_key'
6
+ handles_request_keyword 'list-keys', with_method: 'list_keys'
7
+ handles_request_keyword 'get-key', with_method: 'get_key'
8
+ handles_request_keyword 'fetch-key', with_method: 'fetch_key'
9
+
10
+ def add_key
11
+ results =
12
+ if @mail.has_attachments?
13
+ import_keys_from_attachments
14
+ elsif @mail.first_plaintext_part.body.to_s.present?
15
+ import_key_from_body
16
+ else
17
+ @list.logger.debug 'Found no attachments and an empty body - sending error message'
18
+ I18n.t('keyword_handlers.key_management.no_content_found')
19
+ end
20
+
21
+ import_stati = results.compact.collect(&:imports).flatten
22
+
23
+ if import_stati.blank?
24
+ return I18n.t('keyword_handlers.key_management.no_imports')
25
+ end
26
+
27
+ out = []
28
+
29
+ import_stati.each do |import_status|
30
+ if import_status.action == 'error'
31
+ out << I18n.t('keyword_handlers.key_management.key_import_status.error', fingerprint: import_status.fingerprint)
32
+ else
33
+ key = @list.gpg.find_distinct_key(import_status.fingerprint)
34
+ if key
35
+ out << I18n.t("keyword_handlers.key_management.key_import_status.#{import_status.action}", key_summary: key.summary)
36
+ end
37
+ end
38
+ end
39
+
40
+ out.join("\n\n")
41
+ end
42
+
43
+ def delete_key
44
+ if @arguments.blank?
45
+ return I18n.t(
46
+ 'keyword_handlers.key_management.delete_key_requires_arguments'
47
+ )
48
+ end
49
+
50
+ @arguments.map do |argument|
51
+ # Force GPG to match only fingerprints.
52
+ if argument[0..1] == '0x'
53
+ fingerprint = argument
54
+ else
55
+ fingerprint = "0x#{argument}"
56
+ end
57
+
58
+ keys = @list.keys(fingerprint)
59
+ case keys.size
60
+ when 0
61
+ I18n.t('keyword_handlers.key_management.key_not_found', fingerprint: argument)
62
+ when 1
63
+ begin
64
+ keys.first.delete!
65
+ I18n.t('keyword_handlers.key_management.deleted', key_string: keys.first.summary)
66
+ rescue GPGME::Error::Conflict
67
+ I18n.t('keyword_handlers.key_management.not_deletable', key_string: keys.first.summary)
68
+ end
69
+ else
70
+ # Shouldn't happen, but who knows.
71
+ I18n.t('errors.too_many_matching_keys', {
72
+ input: argument,
73
+ key_strings: keys.map(&:to_s).join("\n")
74
+ })
75
+ end
76
+ end.join("\n\n")
77
+ end
78
+
79
+ def list_keys
80
+ args = Array(@arguments.presence || '')
81
+ args.map do |argument|
82
+ # In this case it shall be allowed to match keys by arbitrary
83
+ # sub-strings, therefore we use `list.gpg` directly to not have the
84
+ # input filtered.
85
+ @list.gpg.keys(argument).map do |key|
86
+ key.to_s
87
+ end
88
+ end.join("\n\n")
89
+ end
90
+
91
+ def get_key
92
+ @arguments.map do |argument|
93
+ keys = @list.keys(argument)
94
+ if keys.blank?
95
+ I18n.t('errors.no_match_for', input: argument)
96
+ else
97
+ result = [I18n.t('keyword_handlers.key_management.matching_keys_intro', input: argument)]
98
+ keys.each do |key|
99
+ atchm = Mail::Part.new
100
+ atchm.body = key.armored
101
+ atchm.content_type = 'application/pgp-keys'
102
+ atchm.content_disposition = "attachment; filename=#{key.fingerprint}.asc"
103
+ result << atchm
104
+ end
105
+ result.flatten
106
+ end
107
+ end
108
+ end
109
+
110
+ def fetch_key
111
+ if @arguments.blank?
112
+ return I18n.t(
113
+ 'keyword_handlers.key_management.fetch_key_requires_arguments'
114
+ )
115
+ end
116
+
117
+ @arguments.map do |argument|
118
+ @list.fetch_keys(argument)
119
+ end
120
+ end
121
+
122
+
123
+ private
124
+
125
+
126
+ def import_keys_from_attachments
127
+ @mail.attachments.map do |attachment|
128
+ import_from_string(attachment.body.raw_source)
129
+ end
130
+ end
131
+
132
+ def import_key_from_body
133
+ [import_from_string(@mail.first_plaintext_part.body.to_s)]
134
+ end
135
+
136
+ def import_from_string(string)
137
+ @list.import_key(string)
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,19 @@
1
+ module Schleuder
2
+ module KeywordHandlers
3
+ class ListManagement < Base
4
+ handles_request_keyword 'get-logfile', with_method: 'get_logfile'
5
+
6
+ def get_logfile
7
+ if File.readable?(@list.logfile)
8
+ attachment = Mail::Part.new
9
+ attachment.body = File.read(@list.logfile)
10
+ attachment.content_disposition = "inline; filename=#{@list.email}.log"
11
+ intro = I18n.t('keyword_handlers.list_management.logfile_attached', listname: @list.email)
12
+ [intro, attachment]
13
+ else
14
+ I18n.t('keyword_handlers.list_management.no_logfile', listname: @list.email)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,208 @@
1
+ module Schleuder
2
+ module KeywordHandlers
3
+ class Resend < Base
4
+ handles_list_keyword 'resend-encrypted-only', with_method: :resend_encrypted_only, has_aliases: 'resend-enc'
5
+
6
+ handles_list_keyword 'resend', with_method: :resend
7
+
8
+ handles_list_keyword 'resend-cc', with_method: :resend_cc
9
+
10
+ handles_list_keyword 'resend-cc-encrypted-only', with_method: :resend_cc_encrypted_only, has_aliases: 'resend-cc-enc'
11
+
12
+ handles_list_keyword 'resend-unencrypted', with_method: :resend_unencrypted
13
+
14
+ handles_list_keyword 'resend-cc-unencrypted', with_method: :resend_cc_unencrypted
15
+
16
+
17
+ def resend
18
+ resend_it
19
+ end
20
+
21
+ def resend_encrypted_only
22
+ resend_it(encrypted_only: true)
23
+ end
24
+
25
+ def resend_cc
26
+ resend_it_cc
27
+ end
28
+
29
+ def resend_cc_encrypted_only
30
+ resend_it_cc(encrypted_only: true)
31
+ end
32
+
33
+ def resend_unencrypted
34
+ do_resend_unencrypted(:to)
35
+ end
36
+
37
+ def resend_cc_unencrypted
38
+ do_resend_unencrypted(:cc)
39
+ end
40
+
41
+
42
+ private
43
+
44
+
45
+ def do_resend_unencrypted(target)
46
+ if ! resend_recipients_valid?
47
+ return false
48
+ end
49
+
50
+ recip_map = Hash[Array(@arguments).map { |email| [email, ''] } ]
51
+
52
+ if do_resend(recip_map, target, false)
53
+ mail.add_subject_prefix_out!
54
+ end
55
+ end
56
+
57
+ def resend_it_cc(encrypted_only: false)
58
+ if ! resend_recipients_valid?
59
+ return false
60
+ end
61
+
62
+ recip_map = map_with_keys(encrypted_only: encrypted_only)
63
+
64
+ # Only continue if all recipients are still here.
65
+ if recip_map.size < @arguments.size
66
+ recip_map.keys.each do |aborted_sender|
67
+ @mail.add_pseudoheader(:error, I18n.t('keyword_handlers.resend.aborted', email: aborted_sender))
68
+ end
69
+ return
70
+ end
71
+
72
+ if do_resend(recip_map, :cc, encrypted_only)
73
+ @mail.add_subject_prefix_out!
74
+ end
75
+ end
76
+
77
+ def resend_it(encrypted_only: false)
78
+ if ! resend_recipients_valid?
79
+ return false
80
+ end
81
+
82
+ recip_map = map_with_keys(encrypted_only: encrypted_only)
83
+
84
+ resent_stati = recip_map.map do |email, key|
85
+ do_resend({email => key}, :to, encrypted_only)
86
+ end
87
+
88
+ if resent_stati.include?(true)
89
+ # At least one message has been resent
90
+ @mail.add_subject_prefix_out!
91
+ end
92
+ end
93
+
94
+ def do_resend(recipients_map, to_or_cc, encrypted_only)
95
+ if recipients_map.empty?
96
+ return
97
+ end
98
+
99
+ gpg_opts = make_gpg_opts(recipients_map, encrypted_only)
100
+ if gpg_opts == false
101
+ return false
102
+ end
103
+
104
+ # Compose and send email
105
+ new = @mail.clean_copy
106
+ new[to_or_cc] = recipients_map.keys
107
+ new.add_public_footer!
108
+ new.sender = @list.bounce_address
109
+ # `dup` gpg_opts because `deliver` changes their value and we need them
110
+ # below to determine encryption!
111
+ new.gpg gpg_opts.dup
112
+
113
+ if new.deliver
114
+ add_resent_headers(recipients_map, to_or_cc, gpg_opts[:encrypt])
115
+ return true
116
+ else
117
+ add_error_header(recipients_map)
118
+ return false
119
+ end
120
+ rescue Net::SMTPFatalError => exc
121
+ add_error_header(recipients_map)
122
+ logger.error "Error while sending: #{exc}"
123
+ return false
124
+ end
125
+
126
+ def map_with_keys(encrypted_only:)
127
+ Array(@arguments).inject({}) do |hash, email|
128
+ keys = @list.keys(email)
129
+ # Exclude unusable keys.
130
+ usable_keys = keys.select { |key| key.usable_for?(:encrypt) }
131
+ case usable_keys.size
132
+ when 1
133
+ hash[email] = usable_keys.first
134
+ when 0
135
+ if encrypted_only
136
+ # Don't add the email to the result to exclude it from the
137
+ # recipients.
138
+ add_resend_msg(email, :error, 'not_resent_no_keys', usable_keys.size, keys.size)
139
+ else
140
+ hash[email] = ''
141
+ end
142
+ else
143
+ # Always report this situation, regardless of sending or not. It's
144
+ # bad and should be fixed.
145
+ add_resend_msg(email, :notice, 'not_resent_encrypted_no_keys', usable_keys.size, keys.size)
146
+ if ! encrypted_only
147
+ hash[email] = ''
148
+ end
149
+ end
150
+ hash
151
+ end
152
+ end
153
+
154
+ def make_gpg_opts(recipients_map, encrypted_only)
155
+ gpg_opts = @list.gpg_sign_options
156
+ # Do all recipients have a key?
157
+ if recipients_map.values.map(&:class).uniq == [GPGME::Key]
158
+ gpg_opts.merge!(encrypt: true)
159
+ elsif encrypted_only
160
+ false
161
+ end
162
+ gpg_opts
163
+ end
164
+
165
+ def add_resend_msg(email, severity, msg, usable_keys_size, all_keys_size)
166
+ @mail.add_pseudoheader(severity, I18n.t("keyword_handlers.resend.#{msg}", email: email, usable_keys: usable_keys_size, all_keys: all_keys_size))
167
+ end
168
+
169
+ def add_error_header(recipients_map)
170
+ @mail.add_pseudoheader(:error, "Resending to #{recipients_map.keys.join(', ')} failed, please check the logs!")
171
+ end
172
+
173
+ def add_resent_headers(recipients_map, to_or_cc, sent_encrypted)
174
+ if sent_encrypted
175
+ prefix = I18n.t('keyword_handlers.resend.encrypted_to')
176
+ str = "\n" + recipients_map.map do |email, key|
177
+ "#{email} (#{key.fingerprint})"
178
+ end.join(",\n")
179
+ else
180
+ prefix = I18n.t('keyword_handlers.resend.unencrypted_to')
181
+ str = ' ' + recipients_map.keys.join(', ')
182
+ end
183
+ headername = resent_header_name(to_or_cc)
184
+ @mail.add_pseudoheader(headername, "#{prefix}#{str}")
185
+ end
186
+
187
+ def resent_header_name(to_or_cc)
188
+ if to_or_cc.to_s == 'to'
189
+ 'resent'
190
+ else
191
+ 'resent_cc'
192
+ end
193
+ end
194
+
195
+ def resend_recipients_valid?
196
+ all_valid = true
197
+ Array(@arguments).each do |address|
198
+ if ! address.match(Conf::EMAIL_REGEXP)
199
+ mail.add_pseudoheader(:error, I18n.t('keyword_handlers.resend.invalid_recipient', address: address))
200
+ all_valid = false
201
+ end
202
+ end
203
+ all_valid
204
+ end
205
+
206
+ end
207
+ end
208
+ end