schleuder 4.0.2 → 5.0.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 (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
+