schleuder 4.0.2 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -9
  3. data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +1 -1
  4. data/db/migrate/20211106112020_change_boolean_values_to_integers.rb +46 -0
  5. data/db/migrate/20211107151309_add_limits_to_string_columns.rb +28 -0
  6. data/db/migrate/20220910170110_add_key_auto_import_from_email.rb +11 -0
  7. data/db/schema.rb +16 -16
  8. data/etc/list-defaults.yml +16 -0
  9. data/etc/schleuder.yml +29 -11
  10. data/lib/schleuder/cli.rb +15 -2
  11. data/lib/schleuder/conf.rb +23 -3
  12. data/lib/schleuder/email_key_importer.rb +91 -0
  13. data/lib/schleuder/filters/post_decryption/35_key_auto_import_from_attachments.rb +21 -0
  14. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +1 -1
  15. data/lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb +36 -4
  16. data/lib/schleuder/filters/pre_decryption/60_key_auto_import_from_autocrypt_header.rb +9 -0
  17. data/lib/schleuder/filters_runner.rb +1 -30
  18. data/lib/schleuder/gpgme/ctx.rb +34 -93
  19. data/lib/schleuder/gpgme/key.rb +1 -1
  20. data/lib/schleuder/gpgme/key_extractor.rb +30 -0
  21. data/lib/schleuder/http.rb +56 -0
  22. data/lib/schleuder/key_fetcher.rb +89 -0
  23. data/lib/schleuder/keyword_handlers/key_management.rb +2 -2
  24. data/lib/schleuder/keyword_handlers/subscription_management.rb +19 -3
  25. data/lib/schleuder/list.rb +26 -10
  26. data/lib/schleuder/list_builder.rb +1 -1
  27. data/lib/schleuder/logger.rb +1 -1
  28. data/lib/schleuder/mail/gpg/decrypted_part.rb +20 -0
  29. data/lib/schleuder/mail/gpg/delivery_handler.rb +38 -0
  30. data/lib/schleuder/mail/gpg/encrypted_part.rb +29 -5
  31. data/lib/schleuder/mail/gpg/gpgme_ext.rb +8 -0
  32. data/lib/schleuder/mail/gpg/gpgme_helper.rb +155 -0
  33. data/lib/schleuder/mail/gpg/inline_decrypted_message.rb +82 -0
  34. data/lib/schleuder/mail/gpg/inline_signed_message.rb +73 -0
  35. data/lib/schleuder/mail/gpg/mime_signed_message.rb +28 -0
  36. data/lib/schleuder/mail/gpg/missing_keys_error.rb +6 -0
  37. data/lib/schleuder/mail/gpg/sign_part.rb +19 -9
  38. data/lib/schleuder/mail/gpg/signed_part.rb +37 -0
  39. data/lib/schleuder/mail/gpg/verified_part.rb +10 -0
  40. data/lib/schleuder/mail/gpg/verify_result_attribute.rb +32 -0
  41. data/lib/schleuder/mail/gpg/version_part.rb +22 -0
  42. data/lib/schleuder/mail/gpg.rb +236 -7
  43. data/lib/schleuder/mail/message.rb +98 -14
  44. data/lib/schleuder/runner.rb +40 -10
  45. data/lib/schleuder/sks_client.rb +18 -0
  46. data/lib/schleuder/version.rb +1 -1
  47. data/lib/schleuder/vks_client.rb +24 -0
  48. data/lib/schleuder-api-daemon/routes/key.rb +22 -1
  49. data/lib/schleuder.rb +11 -7
  50. data/locales/de.yml +38 -19
  51. data/locales/en.yml +22 -3
  52. metadata +58 -21
@@ -7,6 +7,9 @@ module GPGME
7
7
  'new_subkeys' => 8
8
8
  }
9
9
 
10
+ # This differs from import_filtered() in that it doesn't filter the keys at
11
+ # all, and that it returns the import-results themselves, not strings based
12
+ # on those results.
10
13
  def keyimport(keydata)
11
14
  self.import_keys(GPGME::Data.new(keydata))
12
15
  result = self.import_result
@@ -33,13 +36,11 @@ module GPGME
33
36
  end
34
37
 
35
38
  def find_keys(input=nil, secret_only=nil)
36
- _, input = clean_and_classify_input(input)
37
- keys(input, secret_only)
39
+ keys(normalize_key_identifier(input), secret_only)
38
40
  end
39
41
 
40
42
  def find_distinct_key(input=nil, secret_only=nil)
41
- _, input = clean_and_classify_input(input)
42
- keys = keys(input, secret_only)
43
+ keys = keys(normalize_key_identifier(input), secret_only)
43
44
  if keys.size == 1
44
45
  keys.first
45
46
  else
@@ -47,16 +48,16 @@ module GPGME
47
48
  end
48
49
  end
49
50
 
50
- def clean_and_classify_input(input)
51
+ def normalize_key_identifier(input)
51
52
  case input
52
53
  when /.*?([^ <>]+@[^ <>]+).*?/
53
- [:email, "<#{$1}>"]
54
+ "<#{$1}>"
54
55
  when /^http/
55
- [:url, input]
56
+ input
56
57
  when Conf::FINGERPRINT_REGEXP
57
- [:fingerprint, "0x#{input.gsub(/^0x/, '')}"]
58
+ "0x#{input.gsub(/^0x/, '')}"
58
59
  else
59
- [nil, input]
60
+ input
60
61
  end
61
62
  end
62
63
 
@@ -88,85 +89,23 @@ module GPGME
88
89
  GPGME::Engine.info.find {|e| e.protocol == GPGME::PROTOCOL_OpenPGP }
89
90
  end
90
91
 
91
- def refresh_keys(keys)
92
- # reorder keys so the update pattern is random
93
- output = keys.shuffle.map do |key|
94
- # Sleep a short while to make traffic analysis less easy.
95
- sleep rand(1.0..5.0)
96
- refresh_key(key.fingerprint).presence
97
- end
98
- `gpgconf --kill dirmngr`
99
- output.compact.join("\n")
100
- end
101
-
102
- def refresh_key(fingerprint)
103
- args = "#{keyserver_arg} #{import_filter_arg} --refresh-keys #{fingerprint}"
104
- gpgerr, gpgout, exitcode = self.class.gpgcli(args)
105
-
106
- if exitcode > 0
107
- # Return filtered error messages. Include gpgkeys-messages from stdout
108
- # (gpg 2.0 does that), which could e.g. report a failure to connect to
109
- # the keyserver.
110
- # TODO: Revisit this once we don't do network access via GPG
111
- # anymore.
112
- res = [
113
- refresh_key_filter_messages(gpgerr),
114
- refresh_key_filter_messages(gpgout).grep(/^gpgkeys: /)
115
- ].flatten.compact
116
- # if there was an error that we don't filter out,
117
- # we better kill dirmngr, so it hopefully won't suffer
118
- # from the same error during the next run.
119
- # See #309 for background
120
- if !res.empty?
121
- `gpgconf --kill dirmngr`
122
- end
123
- res.join("\n")
124
- else
125
- lines = translate_output('key_updated', gpgout).reject do |line|
126
- # Reduce the noise a little.
127
- line.match(/.* \(unchanged\):$/)
92
+ def import_filtered(input, gpg_extra_arg='')
93
+ # Import through gpgcli so we can use import-filter. GPGME still does
94
+ # not provide that feature (as of summer 2023): <https://dev.gnupg.org/T4721> :(
95
+ gpgerr, gpgout, exitcode = self.class.gpgcli("#{import_filter_arg} #{gpg_extra_arg} --import") do |stdin, stdout, stderr|
96
+ # Wrap this into a block because gpg breaks the pipe if it encounters invalid data.
97
+ begin
98
+ stdin.print input
99
+ rescue Errno::EPIPE
128
100
  end
129
- lines.join("\n")
101
+ stdin.close
102
+ stdout.readlines
130
103
  end
131
- end
132
-
133
- def fetch_key(input)
134
- arguments, error = fetch_key_gpg_arguments_for(input)
135
- return error if error
136
-
137
- gpgerr, gpgout, exitcode = self.class.gpgcli("#{import_filter_arg} #{arguments}")
138
-
139
- # Unfortunately gpg doesn't exit with code > 0 if `--fetch-key` fails.
140
- if exitcode > 0 || gpgerr.grep(/ unable to fetch /).presence
141
- "Fetching #{input} did not succeed:\n#{gpgerr.join("\n")}"
142
- else
143
- translate_output('key_fetched', gpgout).join("\n")
144
- end
145
- end
146
-
147
- def fetch_key_gpg_arguments_for(input)
148
- case input
149
- when Conf::FINGERPRINT_REGEXP
150
- "#{keyserver_arg} --recv-key #{input}"
151
- when /^http/
152
- "--fetch-key #{input}"
153
- when /@/
154
- # --recv-key doesn't work with email-addresses, so we use --locate-key
155
- # restricted to keyservers.
156
- "#{keyserver_arg} --auto-key-locate keyserver --locate-key #{input}"
104
+ if exitcode > 0
105
+ RuntimeError.new(gpgerr.join("\n"))
157
106
  else
158
- [nil, I18n.t('fetch_key.invalid_input')]
159
- end
160
- end
161
-
162
- def translate_output(locale_key, gpgoutput)
163
- import_states = translate_import_data(gpgoutput)
164
- strings = import_states.map do |fingerprint, states|
165
- key = find_distinct_key(fingerprint)
166
- I18n.t(locale_key, key_summary: key.summary,
167
- states: states.to_sentence)
107
+ translate_import_data(gpgout)
168
108
  end
169
- strings
170
109
  end
171
110
 
172
111
  def translate_import_data(gpgoutput)
@@ -210,7 +149,7 @@ module GPGME
210
149
  errors = []
211
150
  output = []
212
151
  base_cmd = gpg_engine.file_name
213
- base_args = '--no-greeting --no-permission-warning --quiet --armor --trust-model always --no-tty --command-fd 0 --status-fd 1'
152
+ base_args = '--no-greeting --quiet --armor --trust-model always --no-tty --command-fd 0 --status-fd 1'
214
153
  cmd = [base_cmd, base_args, args].flatten.join(' ')
215
154
  Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
216
155
  if block_given?
@@ -223,19 +162,21 @@ module GPGME
223
162
  exitcode = thread.value.exitstatus
224
163
  end
225
164
 
165
+ # Don't treat warnings as errors but log them.
166
+ errors = errors.map do |line|
167
+ if line.match?(/gpg: WARNING: (unsafe permissions on homedir|using insecure memory)/i)
168
+ Schleuder.logger.warn(line)
169
+ nil
170
+ else
171
+ line
172
+ end
173
+ end.compact
174
+
226
175
  [errors, output, exitcode]
227
176
  rescue Errno::ENOENT
228
177
  raise 'Need gpg in $PATH or in $GPGBIN'
229
178
  end
230
179
 
231
- def keyserver_arg
232
- if Conf.keyserver.present?
233
- "--keyserver #{Conf.keyserver}"
234
- else
235
- ''
236
- end
237
- end
238
-
239
180
  def import_filter_arg
240
181
  %{ --import-filter drop-sig='sig_created_d > 0000-00-00'}
241
182
  end
@@ -82,7 +82,7 @@ module GPGME
82
82
  end
83
83
 
84
84
  def self.valid_fingerprint?(fp)
85
- fp =~ Schleuder::Conf::FINGERPRINT_REGEXP
85
+ fp.present? && fp.match?(Schleuder::Conf::FINGERPRINT_REGEXP)
86
86
  end
87
87
  end
88
88
  end
@@ -0,0 +1,30 @@
1
+ module GPGME
2
+ class KeyExtractor
3
+ # This takes key material and returns those keys from it, that have a UID
4
+ # matching the given email address, stripped by all other UIDs.
5
+ def self.extract_by_email_address(email_address, keydata)
6
+ orig_gnupghome = ENV['GNUPGHOME']
7
+ ENV['GNUPGHOME'] = Dir.mktmpdir
8
+ gpg = GPGME::Ctx.new(armor: true)
9
+ gpg_arg = %{ --import-filter keep-uid='mbox = #{email_address}'}
10
+ gpg.import_filtered(keydata, gpg_arg)
11
+ # Return the fingerprint and the exported, filtered keydata, because
12
+ # passing the key objects around led to strange problems with some keys,
13
+ # which produced only a blank string as return value of export().
14
+ result = {}
15
+ gpg.keys.each do |tmp_key|
16
+ # Skip this key if it has
17
+ # * no UID – because none survived the import-filter,
18
+ # * more than one UID – which means the import-filtering failed or
19
+ # something else went wrong during import.
20
+ if tmp_key.uids.size == 1
21
+ result[tmp_key.fingerprint] = tmp_key.armored
22
+ end
23
+ end
24
+ result
25
+ ensure
26
+ FileUtils.remove_entry(ENV['GNUPGHOME'])
27
+ ENV['GNUPGHOME'] = orig_gnupghome
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ module Schleuder
2
+ class NetworkError < StandardError; end
3
+
4
+ class NotFoundError < StandardError; end
5
+
6
+ class Http
7
+ attr_reader :request, :response
8
+
9
+ def initialize(url, options={})
10
+ @request = Typhoeus::Request.new(url, default_options.merge(options))
11
+ end
12
+
13
+ def run
14
+ @response = @request.run
15
+ if @response.success?
16
+ @response.body
17
+ elsif @response.timed_out?
18
+ raise_network_error(response, 'HTTP Request timed out.')
19
+ elsif @response.code == 404
20
+ NotFoundError.new
21
+ elsif @response.code == 0
22
+ # This happens e.g. if no response could be received.
23
+ raise_network_error(@response, 'No HTTP response received.')
24
+ else
25
+ RuntimeError.new(@response.body.to_s.presence || @response.return_message)
26
+ end
27
+ end
28
+
29
+ def self.get(url)
30
+ nth_attempt ||= 1
31
+ new(url).run
32
+ rescue NetworkError => error
33
+ nth_attempt += 1
34
+ if nth_attempt < 4
35
+ retry
36
+ else
37
+ return error
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def raise_network_error(response, fallback_msg)
44
+ raise NetworkError.new(
45
+ response.body.to_s.presence || response.return_message || fallback_msg
46
+ )
47
+ end
48
+
49
+ def default_options
50
+ {
51
+ followlocation: true,
52
+ proxy: Conf.http_proxy.presence
53
+ }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,89 @@
1
+ module Schleuder
2
+ class KeyFetcher
3
+ def initialize(list)
4
+ @list = list
5
+ end
6
+
7
+ def fetch(input, locale_key='key_fetched')
8
+ result = case input
9
+ when /^http/
10
+ fetch_key_by_url(input)
11
+ when Conf::EMAIL_REGEXP
12
+ fetch_key_from_keyserver('email', input)
13
+ when Conf::FINGERPRINT_REGEXP
14
+ fetch_key_from_keyserver('fingerprint', input)
15
+ else
16
+ return I18n.t('key_fetcher.invalid_input')
17
+ end
18
+ interpret_fetch_result(result, locale_key)
19
+ end
20
+
21
+ def fetch_key_by_url(url)
22
+ case result = Schleuder::Http.get(url)
23
+ when NotFoundError
24
+ NotFoundError.new(I18n.t('key_fetcher.url_not_found', url: url))
25
+ else
26
+ result
27
+ end
28
+ end
29
+
30
+ def fetch_key_from_keyserver(type, input)
31
+ if Conf.vks_keyserver.present?
32
+ result = Schleuder::VksClient.get(type, input)
33
+ end
34
+ if (result.blank? || ! result.is_a?(String)) && Conf.sks_keyserver.present?
35
+ result = Schleuder::SksClient.get(input)
36
+ end
37
+
38
+ case result
39
+ when nil
40
+ RuntimeError.new('No keyserver configured, cannot query anything')
41
+ when NotFoundError
42
+ NotFoundError.new(I18n.t('key_fetcher.not_found', input: input))
43
+ else
44
+ result
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def interpret_fetch_result(result, locale_key)
51
+ case result
52
+ when ''
53
+ I18n.t('key_fetcher.general_error', error: 'Empty response from server')
54
+ when String
55
+ import(result, locale_key)
56
+ when NotFoundError
57
+ result.to_s
58
+ when StandardError
59
+ I18n.t('key_fetcher.general_error', error: result)
60
+ else
61
+ raise_unexpected_error(result)
62
+ end
63
+ end
64
+
65
+ def import(input, locale_key)
66
+ result = @list.gpg.import_filtered(input)
67
+ case result
68
+ when StandardError
69
+ I18n.t('key_fetcher.import_error', error: result)
70
+ when Hash
71
+ translate_output(locale_key, result).join("\n")
72
+ else
73
+ raise_unexpected_error(result)
74
+ end
75
+ end
76
+
77
+ def translate_output(locale_key, import_states)
78
+ import_states.map do |fingerprint, states|
79
+ key = @list.gpg.find_distinct_key(fingerprint)
80
+ I18n.t(locale_key, key_summary: key.summary,
81
+ states: states.to_sentence)
82
+ end
83
+ end
84
+
85
+ def raise_unexpected_error(thing)
86
+ raise "Unexpected output => #{thing.inspect}"
87
+ end
88
+ end
89
+ end
@@ -15,7 +15,7 @@ module Schleuder
15
15
  import_key_from_body
16
16
  else
17
17
  @list.logger.debug 'Found no attachments and an empty body - sending error message'
18
- I18n.t('keyword_handlers.key_management.no_content_found')
18
+ return I18n.t('keyword_handlers.key_management.no_content_found')
19
19
  end
20
20
 
21
21
  import_stati = results.compact.collect(&:imports).flatten
@@ -125,7 +125,7 @@ module Schleuder
125
125
 
126
126
  def import_keys_from_attachments
127
127
  @mail.attachments.map do |attachment|
128
- import_from_string(attachment.body.raw_source)
128
+ import_from_string(attachment.body.decoded)
129
129
  end
130
130
  end
131
131
 
@@ -22,9 +22,25 @@ module Schleuder
22
22
  while @arguments.first.present? && @arguments.first.match(/^(0x)?[a-f0-9]+$/i)
23
23
  fingerprint << @arguments.shift.downcase
24
24
  end
25
- # Use possibly remaining args as flags.
26
- adminflag = @arguments.shift.to_s.downcase.presence
27
- deliveryflag = @arguments.shift.to_s.downcase.presence
25
+ # If the collected values aren't a valid fingerprint, then the input
26
+ # didn't conform with what this code expects, and then the other
27
+ # values shouldn't be used.
28
+ unless GPGME::Key.valid_fingerprint?(fingerprint)
29
+ return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
30
+ end
31
+ if @arguments.present?
32
+ # Use possibly remaining args as flags.
33
+ adminflag = @arguments.shift.to_s.downcase.presence
34
+ unless ['true', 'false'].include?(adminflag)
35
+ return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
36
+ end
37
+ if @arguments.present?
38
+ deliveryflag = @arguments.shift.to_s.downcase.presence
39
+ unless ['true', 'false'].include?(deliveryflag)
40
+ return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
41
+ end
42
+ end
43
+ end
28
44
  end
29
45
 
30
46
  sub, _ = @list.subscribe(email, fingerprint, adminflag, deliveryflag)
@@ -4,10 +4,10 @@ module Schleuder
4
4
  has_many :subscriptions, dependent: :destroy
5
5
  before_destroy :delete_listdirs
6
6
 
7
- serialize :headers_to_meta, JSON
8
- serialize :bounces_drop_on_headers, JSON
9
- serialize :keywords_admin_only, JSON
10
- serialize :keywords_admin_notify, JSON
7
+ serialize :headers_to_meta, coder: JSON
8
+ serialize :bounces_drop_on_headers, coder: JSON
9
+ serialize :keywords_admin_only, coder: JSON
10
+ serialize :keywords_admin_notify, coder: JSON
11
11
 
12
12
  validates :email, presence: true, uniqueness: true, email: true
13
13
  validates :fingerprint, presence: true, fingerprint: true
@@ -23,7 +23,8 @@ module Schleuder
23
23
  :bounces_notify_admins,
24
24
  :include_list_headers,
25
25
  :include_openpgp_header,
26
- :forward_all_incoming_to_admins, boolean: true
26
+ :forward_all_incoming_to_admins,
27
+ :key_auto_import_from_email, boolean: true
27
28
  validates_each :headers_to_meta,
28
29
  :keywords_admin_only,
29
30
  :keywords_admin_notify do |record, attrib, value|
@@ -197,11 +198,26 @@ module Schleuder
197
198
  end
198
199
 
199
200
  def refresh_keys
200
- gpg.refresh_keys(self.keys)
201
+ # reorder keys so the update pattern is random
202
+ output = self.keys.shuffle.map do |key|
203
+ # Sleep a short while to make traffic analysis less easy.
204
+ sleep rand(1.0..5.0)
205
+ key_fetcher.fetch(key.fingerprint, 'key_updated').presence
206
+ end
207
+ # Filter out some "noise" (if a key was unchanged, it wasn't really updated, was it?)
208
+ # It would be nice to prevent these "false" lines in the first place, but I don't know how.
209
+ output.reject! do |line|
210
+ line.match('updated \(unchanged\)')
211
+ end
212
+ output.compact.join("\n")
201
213
  end
202
214
 
203
215
  def fetch_keys(input)
204
- gpg.fetch_key(input)
216
+ key_fetcher.fetch(input)
217
+ end
218
+
219
+ def key_fetcher
220
+ @key_fetcher ||= KeyFetcher.new(self)
205
221
  end
206
222
 
207
223
  def self.by_recipient(recipient)
@@ -350,7 +366,7 @@ module Schleuder
350
366
  next
351
367
  end
352
368
 
353
- if ! self.deliver_selfsent && incoming_mail.was_validly_signed? && ( subscription == incoming_mail.signer )
369
+ if ! self.deliver_selfsent && incoming_mail&.was_validly_signed? && ( subscription == incoming_mail&.signer )
354
370
  logger.info "Not sending to #{subscription.email}: delivery of self sent is disabled."
355
371
  next
356
372
  end
@@ -373,14 +389,14 @@ module Schleuder
373
389
  end
374
390
 
375
391
  def delete_listdirs
376
- if File.exists?(self.listdir)
392
+ if File.exist?(self.listdir)
377
393
  FileUtils.rm_rf(self.listdir, secure: true)
378
394
  Schleuder.logger.info "Deleted #{self.listdir}"
379
395
  end
380
396
  # If listlogs_dir is different from lists_dir, the logfile still exists
381
397
  # and needs to be deleted, too.
382
398
  logfile_dir = File.dirname(self.logfile)
383
- if File.exists?(logfile_dir)
399
+ if File.exist?(logfile_dir)
384
400
  FileUtils.rm_rf(logfile_dir, secure: true)
385
401
  Schleuder.logger.info "Deleted #{logfile_dir}"
386
402
  end
@@ -122,7 +122,7 @@ module Schleuder
122
122
  end
123
123
 
124
124
  def create_or_test_dir(dir)
125
- if File.exists?(dir)
125
+ if File.exist?(dir)
126
126
  if ! File.directory?(dir)
127
127
  raise Errors::ListdirProblem.new(dir, :not_a_directory)
128
128
  end
@@ -9,7 +9,7 @@ module Schleuder
9
9
  def initialize
10
10
  super('Schleuder', Syslog::LOG_MAIL)
11
11
  # We need some sender-address different from the superadmin-address.
12
- @from = "#{Etc.getlogin}@#{Socket.gethostname}"
12
+ @from = "#{Etc.getlogin}@#{Addrinfo.getaddrinfo(Socket.gethostname, nil).first.getnameinfo.first}"
13
13
  @adminaddresses = Conf.superadmin
14
14
  @level = ::Logger.const_get(Conf.log_level.upcase)
15
15
  end
@@ -0,0 +1,20 @@
1
+ require 'schleuder/mail/gpg/verified_part'
2
+ module Mail
3
+ module Gpg
4
+ class DecryptedPart < VerifiedPart
5
+
6
+ # options are:
7
+ #
8
+ # :verify: decrypt and verify
9
+ def initialize(cipher_part, options = {})
10
+ if cipher_part.mime_type != EncryptedPart::CONTENT_TYPE
11
+ raise EncodingError.new("RFC 3156 incorrect mime type for encrypted part '#{cipher_part.mime_type}'")
12
+ end
13
+
14
+ decrypted = GpgmeHelper.decrypt(cipher_part.body.decoded, options)
15
+ self.verify_result = decrypted.verify_result if options[:verify]
16
+ super(decrypted)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,38 @@
1
+ module Mail
2
+ module Gpg
3
+ class DeliveryHandler
4
+
5
+ def self.deliver_mail(mail)
6
+ if mail.gpg
7
+ encrypted_mail = nil
8
+ begin
9
+ options = mail.gpg.is_a?(TrueClass) ? { encrypt: true } : mail.gpg
10
+ if options[:encrypt]
11
+ encrypted_mail = Mail::Gpg.encrypt(mail, options)
12
+ elsif options[:sign] || options[:sign_as]
13
+ encrypted_mail = Mail::Gpg.sign(mail, options)
14
+ else
15
+ # encrypt and sign are off -> do not encrypt or sign
16
+ yield
17
+ end
18
+ rescue StandardError
19
+ raise $! if mail.raise_encryption_errors
20
+ end
21
+ if encrypted_mail
22
+ if dm = mail.delivery_method
23
+ encrypted_mail.instance_variable_set :@delivery_method, dm
24
+ end
25
+ encrypted_mail.perform_deliveries = mail.perform_deliveries
26
+ encrypted_mail.raise_delivery_errors = mail.raise_delivery_errors
27
+ encrypted_mail.deliver
28
+ end
29
+ else
30
+ yield
31
+ end
32
+ rescue StandardError
33
+ raise $! if mail.raise_delivery_errors
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -1,13 +1,37 @@
1
- module Mail
2
- module Gpg
3
- class EncryptedPart < Mail::Part
4
- alias_method :initialize_mailgpg, :initialize
1
+ module Mail
2
+ module Gpg
3
+ class EncryptedPart < Mail::Part
5
4
 
5
+ CONTENT_TYPE = 'application/octet-stream'
6
+
7
+ # options are:
8
+ #
9
+ # :signers : sign using this key (give the corresponding email address)
10
+ # :password: passphrase for the signing key
11
+ # :recipients : array of receiver addresses
12
+ # :keys : A hash mapping recipient email addresses to public keys or public
13
+ # key ids. Imports any keys given here that are not already part of the
14
+ # local keychain before sending the mail. If this option is given, strictly
15
+ # only the key material from this hash is used, ignoring any keys for
16
+ # recipients that might have been added to the local key chain but are
17
+ # not mentioned here.
18
+ # :always_trust : send encrypted mail to untrusted receivers, true by default
19
+ # :filename : define a custom name for the encrypted file attachment
6
20
  def initialize(cleartext_mail, options = {})
7
21
  if cleartext_mail.protected_headers_subject
8
22
  cleartext_mail.content_type_parameters['protected-headers'] = 'v1'
9
23
  end
10
- initialize_mailgpg(cleartext_mail, options)
24
+
25
+ options = { always_trust: true }.merge options
26
+
27
+ encrypted = GpgmeHelper.encrypt(cleartext_mail.encoded, options)
28
+ super() do
29
+ body encrypted.to_s
30
+ filename = options[:filename] || 'encrypted.asc'
31
+ content_type "#{CONTENT_TYPE}; name=\"#{filename}\""
32
+ content_disposition "inline; filename=\"#{filename}\""
33
+ content_description 'OpenPGP encrypted message'
34
+ end
11
35
  end
12
36
  end
13
37
  end
@@ -0,0 +1,8 @@
1
+ require 'schleuder/mail/gpg/verify_result_attribute'
2
+
3
+ # extend GPGME::Data with an attribute to hold the result of signature
4
+ # verifications
5
+ class GPGME::Data
6
+ include Mail::Gpg::VerifyResultAttribute
7
+ end
8
+