mail-gpg 0.4.4 → 0.4.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 96edf0fbe3f22c01808d60328a9d316d4cab6588
4
- data.tar.gz: dbe492f6351962e3696eb13a8185bc99f0ded52a
2
+ SHA256:
3
+ metadata.gz: 3acca2c34af794c59ff717e0f29d27b26ed1d1e095dd388a6238e0ef31366644
4
+ data.tar.gz: c1161da992e780ecf3c6bea38977c9b8d0c86b2a5530f2095e398f868e7a70c9
5
5
  SHA512:
6
- metadata.gz: 6ef811441d350386921a18d46d5b4aff71867eacb346565a5a1583eb8e516c3f0643b065cb1e02cca46db7cfa0333b5ed82435aab6da6bed4a9ecf5b34c054aa
7
- data.tar.gz: 2588e6bbce89a8dd1fde81c582352c40b694a8a9c1de1b71ec57fbbf50bbf75cb770f75da3b822e25034abace1d210958a557bf7b224e8e305cbcaf2c2e7887f
6
+ metadata.gz: ac9858c3c4838fa909cec047658a37945fa37ebf08a6b8a005ca336aab4543caed549f4a4c54a614195084772b9baa728978e2db80f2e1582767ea9a888ac743
7
+ data.tar.gz: b082facfd7d4c84a62e40bcf3aaba4866121daa0a65d8a962164031f7989eca572b5811187fb8f41dfb4948dbd7191aea4f1cce44d768c488721334d70073223
@@ -0,0 +1,41 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ include:
17
+ - ruby-version: "2.7.8"
18
+ rails-version: "5.2.8.1"
19
+ - ruby-version: "3.2"
20
+ rails-version: "8.0"
21
+ - ruby-version: "3.4"
22
+ rails-version: "8.0"
23
+
24
+ # picked up by the Gemfile: gem 'actionmailer', "~> #{ENV['RAILS']}"
25
+ env:
26
+ RAILS: ${{ matrix.rails-version }}
27
+
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Install GPG
32
+ run: sudo apt-get update && sudo apt-get install -y gnupg
33
+
34
+ - name: Set up Ruby ${{ matrix.ruby-version }}
35
+ uses: ruby/setup-ruby@v1
36
+ with:
37
+ ruby-version: ${{ matrix.ruby-version }}
38
+ bundler-cache: true
39
+
40
+ - name: Run tests
41
+ run: bundle exec rake
data/History.txt CHANGED
@@ -1,3 +1,24 @@
1
+ == 0.4.6 2026-08-13
2
+
3
+ * fix double transfer-decoding of inline signed / encrypted mails: the
4
+ rebuilt message kept the original Content-Transfer-Encoding header while
5
+ its body was set to the already decoded (verified / decrypted) text, so
6
+ reading the body would corrupt it (e.g. base64-decode plain text)
7
+ * modernize test setup: RSA 2048 test keys generated without passphrase
8
+ protection (current GnuPG refuses to sign with the old DSA 1024 keys, and
9
+ interactive pinentry prompts no longer get in the way); bad passphrase
10
+ handling is now tested with a dedicated protected key via loopback
11
+ pinentry
12
+ * CI runs on GitHub Actions with Ruby 2.7.8 / Rails 5.2.8.1 and
13
+ Ruby 3.2 / 3.4 with Rails 8
14
+
15
+ == 0.4.5 2026-01-20
16
+
17
+ * fix HKP client for Ruby 3.0+ (replaced deprecated URI.escape/decode)
18
+ * change default keyserver from defunct pool.sks-keyservers.net to
19
+ keyserver.ubuntu.com
20
+ * fix HKP client redirect handling
21
+
1
22
  == 0.4.4 2020-07-30
2
23
 
3
24
  * preserve Content-ID header of signed parts (relevant for inline images in
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Mail::Gpg [![Build Status](https://travis-ci.org/jkraemer/mail-gpg.png?branch=master)](https://travis-ci.org/jkraemer/mail-gpg)
1
+ # Mail::Gpg [![Tests](https://github.com/jkraemer/mail-gpg/actions/workflows/test.yml/badge.svg)](https://github.com/jkraemer/mail-gpg/actions/workflows/test.yml)
2
2
 
3
3
  This gem adds GPG/MIME encryption capabilities to the [Ruby Mail
4
4
  Library](https://github.com/mikel/mail)
data/lib/hkp.rb CHANGED
@@ -49,7 +49,16 @@ class Hkp
49
49
  if redirect_depth >= MAX_REDIRECTS
50
50
  raise TooManyRedirects
51
51
  else
52
- http_get response['location'], redirect_depth + 1
52
+ location = URI.parse(response['location'])
53
+ if location.host && (location.host != @host || location.port != @port)
54
+ # Cross-host redirect - create new client for the target server
55
+ redirect_client = Client.new(response['location'], ssl_verify_mode: @ssl_verify_mode)
56
+ return redirect_client.get(location.request_uri, redirect_depth + 1)
57
+ else
58
+ # Same-host redirect (relative path or same host)
59
+ redirect_path = location.respond_to?(:request_uri) ? location.request_uri : response['location']
60
+ return get(redirect_path, redirect_depth + 1)
61
+ end
53
62
  end
54
63
  else
55
64
  raise InvalidResponse, response.code
@@ -65,7 +74,7 @@ class Hkp
65
74
  if String === options
66
75
  options = { keyserver: options }
67
76
  end
68
- @keyserver = options.delete(:keyserver) || lookup_keyserver || 'http://pool.sks-keyservers.net:11371'
77
+ @keyserver = options.delete(:keyserver) || lookup_keyserver || 'hkp://keyserver.ubuntu.com'
69
78
  @options = { raise_errors: true }.merge options
70
79
  end
71
80
 
@@ -83,7 +92,7 @@ class Hkp
83
92
  # and what info they return besides the key id
84
93
  def search(name)
85
94
  [].tap do |results|
86
- result = hkp_client.get "/pks/lookup?options=mr&search=#{URI.escape name}"
95
+ result = hkp_client.get "/pks/lookup?op=index&options=mr&search=#{URI.encode_www_form_component(name)}"
87
96
 
88
97
  result.each_line do |l|
89
98
  components = l.strip.split(':')
@@ -101,7 +110,7 @@ class Hkp
101
110
 
102
111
  # returns the key data as returned from the server as a string
103
112
  def fetch(id)
104
- result = hkp_client.get "/pks/lookup?options=mr&op=get&search=0x#{URI.escape id}"
113
+ result = hkp_client.get "/pks/lookup?op=get&options=mr&search=0x#{URI.encode_www_form_component(id)}"
105
114
  return clean_key(result) if result
106
115
 
107
116
  rescue Exception
@@ -142,7 +151,7 @@ class Hkp
142
151
  def lookup_keyserver
143
152
  url = nil
144
153
  if res = exec_cmd("gpgconf --list-options gpgs 2>&1 | grep keyserver 2>&1")
145
- url = URI.decode(res.split(":").last.split("\"").last.strip)
154
+ url = URI.decode_www_form_component(res.split(":").last.split("\"").last.strip)
146
155
  elsif res = exec_cmd("gpg --gpgconf-list 2>&1 | grep gpgconf-gpg.conf 2>&1")
147
156
  conf_file = res.split(":").last.split("\"").last.strip
148
157
  if res = exec_cmd("cat #{conf_file} 2>&1 | grep ^keyserver 2>&1")
@@ -59,7 +59,9 @@ module Mail
59
59
  else
60
60
  p.content_type part.content_type
61
61
  p.content_transfer_encoding part.content_transfer_encoding
62
- p.body part.body.to_s
62
+ # keep the raw (still encoded) body to match the transfer
63
+ # encoding copied above - body.to_s would decode it
64
+ p.body part.body.raw_source
63
65
  end
64
66
  end
65
67
  end
@@ -72,6 +74,10 @@ module Mail
72
74
  cipher_mail.header.fields.each do |field|
73
75
  header[field.name] = field.value
74
76
  end
77
+ # the decrypted body set below is not transfer encoded, so an
78
+ # encoding copied over from the original mail would lead to
79
+ # double decoding
80
+ header['Content-Transfer-Encoding'] = nil
75
81
  body decrypted.to_s
76
82
  verify_result decrypted.verify_result if options[:verify] && '' != decrypted
77
83
  end
@@ -17,6 +17,10 @@ module Mail
17
17
  success, vr = GpgmeHelper.inline_verify(signed_text, options)
18
18
  p = VerifiedPart.new(part)
19
19
  if success
20
+ # the body set here is already decoded, so a transfer
21
+ # encoding copied over from the original part would lead to
22
+ # double decoding
23
+ p.header['Content-Transfer-Encoding'] = nil
20
24
  p.body self.class.strip_inline_signature signed_text
21
25
  end
22
26
  p.verify_result vr
@@ -33,6 +37,9 @@ module Mail
33
37
  signed_mail.header.fields.each do |field|
34
38
  header[field.name] = field.value
35
39
  end
40
+ # the body set below is already decoded, so a transfer encoding
41
+ # copied over from the original mail would lead to double decoding
42
+ header['Content-Transfer-Encoding'] = nil
36
43
  signed_text = signed_mail.body.to_s
37
44
  success, vr = GpgmeHelper.inline_verify(signed_text, options)
38
45
  if success
@@ -1,5 +1,5 @@
1
1
  module Mail
2
2
  module Gpg
3
- VERSION = "0.4.4"
3
+ VERSION = "0.4.6"
4
4
  end
5
5
  end
data/mail-gpg.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "actionmailer", ">= 3.2.0"
27
27
  spec.add_development_dependency "byebug"
28
28
  spec.add_development_dependency "shoulda-context", '~> 1.1'
29
+ spec.add_development_dependency "webmock", '~> 3.0'
29
30
  end
data/test/hkp_test.rb CHANGED
@@ -4,7 +4,7 @@ require 'hkp'
4
4
 
5
5
  class HkpTest < MailGpgTestCase
6
6
 
7
- context "hpk client" do
7
+ context "hkp client" do
8
8
  {
9
9
  "http://pool.sks-keyservers.net:11371" => {
10
10
  host: 'pool.sks-keyservers.net',
@@ -99,4 +99,222 @@ class HkpTest < MailGpgTestCase
99
99
 
100
100
  end
101
101
 
102
+ context 'with mocked server' do
103
+ setup do
104
+ WebMock.disable_net_connect!
105
+ @keyserver = 'hkp://keys.example.com'
106
+ @hkp = Hkp.new(keyserver: @keyserver)
107
+ end
108
+
109
+ teardown do
110
+ WebMock.allow_net_connect!
111
+ end
112
+
113
+ context 'search' do
114
+ should 'include op=index parameter in request' do
115
+ stub_request(:get, "http://keys.example.com:11371/pks/lookup")
116
+ .with(query: hash_including('op' => 'index'))
117
+ .to_return(status: 200, body: "info:1:1\npub:ABC123:1:2048:1234567890::\nuid:Test User <test@example.com>:1234567890::")
118
+
119
+ result = @hkp.search('test@example.com')
120
+ assert_requested :get, "http://keys.example.com:11371/pks/lookup",
121
+ query: hash_including('op' => 'index', 'search' => 'test@example.com')
122
+ end
123
+
124
+ should 'properly encode special characters in search query' do
125
+ stub_request(:get, /keys\.example\.com/)
126
+ .to_return(status: 200, body: "info:1:1\npub:ABC123:1:2048:1234567890::\n")
127
+
128
+ @hkp.search('test+user@example.com')
129
+
130
+ # Should use proper URL encoding, + should become %2B
131
+ assert_requested :get, "http://keys.example.com:11371/pks/lookup",
132
+ query: hash_including('search' => 'test+user@example.com')
133
+ end
134
+
135
+ should 'parse machine readable response' do
136
+ stub_request(:get, /keys\.example\.com/)
137
+ .to_return(status: 200, body: <<~RESPONSE)
138
+ info:1:2
139
+ pub:ABC123DEF456:1:2048:1234567890::
140
+ uid:Test User <test@example.com>:1234567890::
141
+ pub:789GHI012JKL:1:4096:1234567891::
142
+ uid:Another User <another@example.com>:1234567891::
143
+ RESPONSE
144
+
145
+ result = @hkp.search('example.com')
146
+
147
+ assert_equal 2, result.size
148
+ assert_equal 'ABC123DEF456', result[0][0]
149
+ assert_equal '789GHI012JKL', result[1][0]
150
+ end
151
+
152
+ should 'return empty array when no keys found' do
153
+ stub_request(:get, /keys\.example\.com/)
154
+ .to_return(status: 200, body: "info:1:0\n")
155
+
156
+ result = @hkp.search('nonexistent@example.com')
157
+
158
+ assert_equal [], result
159
+ end
160
+
161
+ should 'raise error on server error when raise_errors is true' do
162
+ stub_request(:get, /keys\.example\.com/)
163
+ .to_return(status: 500, body: "Internal Server Error")
164
+
165
+ assert_raises(Hkp::InvalidResponse) do
166
+ @hkp.search('test@example.com')
167
+ end
168
+ end
169
+
170
+ should 'return nil on server error when raise_errors is false' do
171
+ hkp = Hkp.new(keyserver: @keyserver, raise_errors: false)
172
+ stub_request(:get, /keys\.example\.com/)
173
+ .to_return(status: 500, body: "Internal Server Error")
174
+
175
+ result = hkp.search('test@example.com')
176
+
177
+ assert_nil result
178
+ end
179
+ end
180
+
181
+ context 'fetch' do
182
+ should 'include op=get parameter in request' do
183
+ stub_request(:get, "http://keys.example.com:11371/pks/lookup")
184
+ .with(query: hash_including('op' => 'get'))
185
+ .to_return(status: 200, body: <<~KEY)
186
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
187
+ mQENBFxxxxxxx
188
+ -----END PGP PUBLIC KEY BLOCK-----
189
+ KEY
190
+
191
+ result = @hkp.fetch('ABC123')
192
+ assert_requested :get, "http://keys.example.com:11371/pks/lookup",
193
+ query: hash_including('op' => 'get', 'search' => '0xABC123')
194
+ end
195
+
196
+ should 'properly encode key id with special characters' do
197
+ stub_request(:get, /keys\.example\.com/)
198
+ .to_return(status: 200, body: <<~KEY)
199
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
200
+ mQENBFxxxxxxx
201
+ -----END PGP PUBLIC KEY BLOCK-----
202
+ KEY
203
+
204
+ # Key IDs shouldn't normally have special chars, but testing encoding anyway
205
+ @hkp.fetch('ABC+123')
206
+
207
+ assert_requested :get, "http://keys.example.com:11371/pks/lookup",
208
+ query: hash_including('search' => '0xABC+123')
209
+ end
210
+
211
+ should 'extract key from response' do
212
+ stub_request(:get, /keys\.example\.com/)
213
+ .to_return(status: 200, body: <<~RESPONSE)
214
+ Some header text
215
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
216
+
217
+ mQENBFxxxxxxx
218
+ =xxxx
219
+ -----END PGP PUBLIC KEY BLOCK-----
220
+ Some footer text
221
+ RESPONSE
222
+
223
+ result = @hkp.fetch('ABC123')
224
+
225
+ assert result.start_with?('-----BEGIN PGP PUBLIC KEY BLOCK-----')
226
+ assert result.end_with?('-----END PGP PUBLIC KEY BLOCK-----')
227
+ refute result.include?('Some header text')
228
+ refute result.include?('Some footer text')
229
+ end
230
+
231
+ should 'return nil when key not found' do
232
+ stub_request(:get, /keys\.example\.com/)
233
+ .to_return(status: 404, body: "Not Found")
234
+
235
+ hkp = Hkp.new(keyserver: @keyserver, raise_errors: false)
236
+ result = hkp.fetch('NONEXISTENT')
237
+
238
+ assert_nil result
239
+ end
240
+
241
+ should 'return nil when response has no valid key block' do
242
+ stub_request(:get, /keys\.example\.com/)
243
+ .to_return(status: 200, body: "No key here")
244
+
245
+ result = @hkp.fetch('ABC123')
246
+
247
+ assert_nil result
248
+ end
249
+ end
250
+
251
+ context 'redirects' do
252
+ should 'follow same-host redirects' do
253
+ stub_request(:get, "http://keys.example.com:11371/pks/lookup")
254
+ .with(query: hash_including('op' => 'index', 'search' => 'test'))
255
+ .to_return(status: 302, headers: { 'Location' => '/pks/lookup?op=index&options=mr&search=test&new=1' })
256
+
257
+ stub_request(:get, "http://keys.example.com:11371/pks/lookup")
258
+ .with(query: hash_including('op' => 'index', 'new' => '1'))
259
+ .to_return(status: 200, body: "info:1:1\npub:ABC123:1:2048:1234567890::\n")
260
+
261
+ result = @hkp.search('test')
262
+ assert_equal 1, result.size
263
+ assert_equal 'ABC123', result[0][0]
264
+ end
265
+
266
+ should 'follow cross-host redirects' do
267
+ stub_request(:get, "http://keys.example.com:11371/pks/lookup")
268
+ .with(query: hash_including('op' => 'index'))
269
+ .to_return(status: 302, headers: { 'Location' => 'http://keys2.example.com/pks/lookup?op=index&options=mr&search=test' })
270
+
271
+ stub_request(:get, "http://keys2.example.com:80/pks/lookup")
272
+ .with(query: hash_including('op' => 'index'))
273
+ .to_return(status: 200, body: "info:1:1\npub:DEF456:1:2048:1234567890::\n")
274
+
275
+ result = @hkp.search('test')
276
+ assert_equal 1, result.size
277
+ assert_equal 'DEF456', result[0][0]
278
+ end
279
+
280
+ should 'raise TooManyRedirects after MAX_REDIRECTS' do
281
+ stub_request(:get, /keys\.example\.com/)
282
+ .to_return(status: 302, headers: { 'Location' => 'http://keys.example.com:11371/pks/lookup?op=index&search=test' })
283
+
284
+ assert_raises(Hkp::TooManyRedirects) do
285
+ @hkp.search('test')
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ context 'lookup_keyserver' do
292
+ setup do
293
+ WebMock.disable_net_connect!
294
+ end
295
+
296
+ teardown do
297
+ WebMock.allow_net_connect!
298
+ end
299
+
300
+ should 'decode percent-encoded keyserver URL from gpgconf' do
301
+ # Create a testable subclass that stubs exec_cmd
302
+ hkp_class = Class.new(Hkp) do
303
+ def exec_cmd(cmd)
304
+ if cmd.include?('gpgconf --list-options')
305
+ # Simulated gpgconf output with percent-encoded URL
306
+ # Format: name:flags:value (value may be quoted)
307
+ "keyserver:0:hkp%3A%2F%2Fkeys.example.com\n"
308
+ else
309
+ nil
310
+ end
311
+ end
312
+ end
313
+
314
+ hkp = hkp_class.new
315
+ keyserver = hkp.instance_variable_get('@keyserver')
316
+ assert_equal 'hkp://keys.example.com', keyserver
317
+ end
318
+ end
319
+
102
320
  end
@@ -31,6 +31,24 @@ class InlineDecryptedMessageTest < MailGpgTestCase
31
31
  assert sig.to_s=~ /Joe/
32
32
  assert sig.valid?
33
33
  end
34
+
35
+ should "decrypt base64 encoded body" do
36
+ mail = Mail.new(@mail)
37
+ cipher_text = InlineDecryptedMessageTest.encrypt(mail, mail.body.to_s)
38
+ mail.content_transfer_encoding 'base64'
39
+ mail.body Mail::Encodings::Base64.encode(cipher_text)
40
+ mail = Mail.new(mail.encoded)
41
+
42
+ assert !mail.multipart?
43
+ assert mail.encrypted?
44
+ assert decrypted = mail.decrypt(:password => 'abc', verify: true)
45
+ assert_equal 'i am unencrypted', decrypted.body.decoded,
46
+ "body was double decoded: #{decrypted.body.decoded.inspect}"
47
+ assert vr = decrypted.verify_result
48
+ assert sig = vr.signatures.first
49
+ assert sig.to_s=~ /Joe/
50
+ assert sig.valid?
51
+ end
34
52
  end
35
53
 
36
54
  context "multipart/alternative message" do
@@ -77,6 +95,35 @@ class InlineDecryptedMessageTest < MailGpgTestCase
77
95
  end
78
96
  end
79
97
 
98
+ context "base64 encoded cleartext part and encrypted attachment message" do
99
+ should "decrypt attachment and not double decode text part" do
100
+ rakefile = File.open('Rakefile') { |file| file.read }
101
+ mail = Mail.new(@mail)
102
+ mail.content_type = 'multipart/mixed'
103
+ mail.body = ''
104
+ mail.part do |p|
105
+ p.content_type 'text/plain'
106
+ p.content_transfer_encoding 'base64'
107
+ p.body Mail::Encodings::Base64.encode('i am unencrypted')
108
+ end
109
+ mail.part do |p|
110
+ p.content_type 'application/octet-stream; name=Rakefile.pgp'
111
+ p.content_transfer_encoding Mail::Encodings::Base64
112
+ p.content_disposition 'attachment; filename="Rakefile.pgp"'
113
+ p.body Mail::Encodings::Base64::encode(InlineDecryptedMessageTest.encrypt(mail, rakefile, false))
114
+ end
115
+
116
+ assert mail.multipart?
117
+ assert mail.encrypted?
118
+ assert decrypted = mail.decrypt(password: 'abc')
119
+ assert !decrypted.encrypted?
120
+ assert_equal 2, decrypted.parts.length
121
+ assert_equal 'i am unencrypted', decrypted.parts[0].body.decoded,
122
+ "part was double decoded: #{decrypted.parts[0].body.decoded.inspect}"
123
+ assert_equal rakefile, decrypted.parts[1].body.decoded
124
+ end
125
+ end
126
+
80
127
  context "cleartext body and encrypted attachment message" do
81
128
  should "decrypt and verify attachment" do
82
129
  rakefile = File.open('Rakefile') { |file| file.read }
@@ -54,9 +54,54 @@ class InlineSignedMessageTest < MailGpgTestCase
54
54
  assert !sig.valid?
55
55
  end
56
56
 
57
+ should "verify base64 encoded body" do
58
+ mail = Mail.new(@mail)
59
+ signed_text = self.class.inline_sign(mail, mail.body.to_s)
60
+ mail.content_transfer_encoding 'base64'
61
+ mail.body Mail::Encodings::Base64.encode(signed_text)
62
+ mail = Mail.new(mail.encoded)
63
+ assert !mail.multipart?
64
+ assert mail.signed?
65
+ assert verified = mail.verify
66
+ assert verified.signature_valid?
67
+ assert verified.body.decoded.include?('i am unencrypted'),
68
+ "body was double decoded: #{verified.body.decoded.inspect}"
69
+ end
70
+
71
+ should "keep base64 encoded body readable when signature is invalid" do
72
+ mail = Mail.new(@mail)
73
+ signed_text = self.class.inline_sign(mail, mail.body.to_s).gsub /i am/, 'i was'
74
+ mail.content_transfer_encoding 'base64'
75
+ mail.body Mail::Encodings::Base64.encode(signed_text)
76
+ mail = Mail.new(mail.encoded)
77
+ assert !mail.multipart?
78
+ assert mail.signed?
79
+ assert verified = mail.verify
80
+ assert !verified.signature_valid?
81
+ assert verified.body.decoded.include?('i was unencrypted'),
82
+ "body was double decoded: #{verified.body.decoded.inspect}"
83
+ end
84
+
57
85
  end
58
86
 
59
87
  context "message with signed attachment" do
88
+ should "check base64 encoded attachment signature" do
89
+ mail = Mail.new(@mail)
90
+ mail.body = 'foobar'
91
+ signed_text = self.class.inline_sign(mail, 'sign me!')
92
+ mail.part do |p|
93
+ p.content_transfer_encoding 'base64'
94
+ p.body Mail::Encodings::Base64.encode(signed_text)
95
+ end
96
+ mail = Mail.new(mail.encoded)
97
+ assert mail.multipart?
98
+ assert mail.signed?
99
+ assert verified = mail.verify
100
+ assert verified.signature_valid?
101
+ assert verified.parts.last.body.decoded.include?('sign me!'),
102
+ "part was double decoded: #{verified.parts.last.body.decoded.inspect}"
103
+ end
104
+
60
105
  should "check attachment signature" do
61
106
  mail = Mail.new(@mail)
62
107
  mail.body = 'foobar'
data/test/message_test.rb CHANGED
@@ -53,8 +53,6 @@ class MessageTest < MailGpgTestCase
53
53
  @mail.deliver
54
54
  @signed = Mail.new @mails.first.to_s
55
55
  @verified = @signed.verify
56
- # Mail gem from 2.7.1 onwards converts "\n" to "\r\n"
57
- @body = Mail::Utilities.to_crlf(@body)
58
56
  end
59
57
 
60
58
  should 'keep body unchanged' do
@@ -88,7 +86,7 @@ class MessageTest < MailGpgTestCase
88
86
  body "and\nanother part euro €"
89
87
  end
90
88
  @mail.add_part p
91
- # if we do not force it to binary, the line ending is changed to CRLF. WTF?
89
+ # attachment bodies are returned as binary
92
90
  @attachment_data = "this is\n € not an image".force_encoding(Encoding::BINARY)
93
91
  @mail.attachments['test.jpg'] = { mime_type: 'image/jpeg',
94
92
  content: @attachment_data }
@@ -106,7 +104,7 @@ class MessageTest < MailGpgTestCase
106
104
  should 'have original three parts' do
107
105
  assert_equal 3, @verified.parts.size
108
106
  assert_equal 'i am unencrypted', @verified.parts[0].body.to_s
109
- assert_equal "and\r\nanother part euro €", @verified.parts[1].body.to_s.force_encoding('UTF-8')
107
+ assert_equal "and\nanother part euro €", @verified.parts[1].body.to_s.force_encoding('UTF-8')
110
108
  assert attachment = @verified.parts[2]
111
109
  assert attachment.attachment?
112
110
  assert_equal "attachment; filename=test.jpg", attachment.content_disposition
@@ -141,8 +139,8 @@ class MessageTest < MailGpgTestCase
141
139
  assert_equal 3, @mail.parts.size
142
140
  assert_equal 3, @verified.parts.size
143
141
  assert_equal 'i am unencrypted', @verified.parts[0].body.to_s
144
- assert_equal "and\r\nanother part euro €", @verified.parts[1].body.to_s.force_encoding('UTF-8')
145
- assert_equal "and an\r\nHTML part €", @verified.parts[2].body.to_s.force_encoding('UTF-8')
142
+ assert_equal "and\nanother part euro €", @verified.parts[1].body.to_s.force_encoding('UTF-8')
143
+ assert_equal "and an\nHTML part €", @verified.parts[2].body.to_s.force_encoding('UTF-8')
146
144
  end
147
145
  end
148
146
 
@@ -227,7 +225,7 @@ class MessageTest < MailGpgTestCase
227
225
  assert decrypted = m.decrypt(:password => 'abc', verify: true)
228
226
  assert_equal 'test', decrypted.subject
229
227
  assert decrypted == @mail
230
- assert_equal "one\r\neuro €", decrypted.body.to_s.force_encoding('UTF-8')
228
+ assert_equal "one\neuro €", decrypted.body.to_s.force_encoding('UTF-8')
231
229
  assert decrypted.signature_valid?
232
230
  assert_equal 1, decrypted.signatures.size
233
231
  end
@@ -300,24 +298,26 @@ class MessageTest < MailGpgTestCase
300
298
  end
301
299
 
302
300
  should "raise bad passphrase on decrypt" do
303
- assert_equal 1, @mails.size
304
- assert m = @mails.first
301
+ # patty's key is passphrase protected; loopback pinentry keeps
302
+ # gpg-agent from prompting interactively
303
+ mail = Mail.new do
304
+ from 'joe@foo.bar'
305
+ to 'patty@foo.bar'
306
+ subject 'test'
307
+ body 'i am unencrypted'
308
+ gpg encrypt: true
309
+ end
310
+ mail.deliver
311
+ assert m = @mails.last
305
312
  assert_equal 'test', m.subject
313
+ assert m.encrypted?
306
314
  # incorrect passphrase
307
- if @gpg_utils.preset_passphrases?
308
- set_passphrase('incorrect')
309
- # expected_exception = GPGME::Error::DecryptFailed
310
- # I dont know why.
311
- expected_exception = EOFError
312
- else
313
- expected_exception = GPGME::Error::BadPassphrase
314
- end
315
- assert_raises(expected_exception){
316
- m.decrypt(:password => 'incorrect')
315
+ assert_raises(GPGME::Error::BadPassphrase){
316
+ m.decrypt(password: 'incorrect', pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK)
317
317
  }
318
- # no passphrase
319
- assert_raises(expected_exception){
320
- m.decrypt
318
+ # no passphrase - gpg gets no data from the loopback pinentry
319
+ assert_raises(GPGME::Error::NoData){
320
+ m.decrypt(pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK)
321
321
  }
322
322
  end
323
323
  end
data/test/test_helper.rb CHANGED
@@ -1,11 +1,15 @@
1
1
  require 'open3'
2
2
  require 'test/unit'
3
3
  require 'shoulda/context'
4
+ require 'webmock/test_unit'
4
5
  require 'mail-gpg'
5
6
  require 'action_mailer'
6
7
  require 'securerandom'
7
8
  require 'byebug'
8
9
 
10
+ # Allow real connections by default, individual tests can disable
11
+ WebMock.allow_net_connect!
12
+
9
13
  Mail.defaults do
10
14
  delivery_method :test
11
15
  end
@@ -67,31 +71,46 @@ class GPGTestUtils
67
71
  def gen_keys
68
72
  puts "setting up keydir #{@home}"
69
73
  FileUtils.mkdir_p @home
70
- (File.open(File.join(@home, "gpg-agent.conf"), "wb") << "allow-preset-passphrase\nbatch\n").close
74
+ (File.open(File.join(@home, "gpg-agent.conf"), "wb") << "allow-preset-passphrase\n").close
71
75
  GPGME::Ctx.new do |gpg|
72
76
  gpg.generate_key <<-END
73
77
  <GnupgKeyParms format="internal">
74
- Key-Type: DSA
75
- Key-Length: 1024
76
- Subkey-Type: ELG-E
77
- Subkey-Length: 1024
78
+ %no-protection
79
+ Key-Type: RSA
80
+ Key-Length: 2048
81
+ Subkey-Type: RSA
82
+ Subkey-Length: 2048
78
83
  Name-Real: Joe Tester
79
84
  Name-Comment: with stupid passphrase
80
85
  Name-Email: joe@foo.bar
81
86
  Expire-Date: 0
82
- Passphrase: abc
83
87
  </GnupgKeyParms>
84
88
  END
85
89
  gpg.generate_key <<-END
86
90
  <GnupgKeyParms format="internal">
87
- Key-Type: DSA
88
- Key-Length: 1024
89
- Subkey-Type: ELG-E
90
- Subkey-Length: 1024
91
+ %no-protection
92
+ Key-Type: RSA
93
+ Key-Length: 2048
94
+ Subkey-Type: RSA
95
+ Subkey-Length: 2048
91
96
  Name-Real: Jane Doe
92
97
  Name-Comment: with stupid passphrase
93
98
  Name-Email: jane@foo.bar
94
99
  Expire-Date: 0
100
+ </GnupgKeyParms>
101
+ END
102
+ # passphrase protected key for testing bad passphrase handling with
103
+ # loopback pinentry
104
+ gpg.generate_key <<-END
105
+ <GnupgKeyParms format="internal">
106
+ Key-Type: RSA
107
+ Key-Length: 2048
108
+ Subkey-Type: RSA
109
+ Subkey-Length: 2048
110
+ Name-Real: Patty Protected
111
+ Name-Comment: with stupid passphrase
112
+ Name-Email: patty@foo.bar
113
+ Expire-Date: 0
95
114
  Passphrase: abc
96
115
  </GnupgKeyParms>
97
116
  END
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mail-gpg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jens Kraemer
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-30 00:00:00.000000000 Z
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail
@@ -134,6 +134,20 @@ dependencies:
134
134
  - - "~>"
135
135
  - !ruby/object:Gem::Version
136
136
  version: '1.1'
137
+ - !ruby/object:Gem::Dependency
138
+ name: webmock
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '3.0'
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '3.0'
137
151
  description: |-
138
152
  GPG/MIME encryption plugin for the Ruby Mail Library
139
153
  This tiny gem adds GPG capabilities to Mail::Message and ActionMailer::Base. Because privacy matters.
@@ -143,8 +157,8 @@ executables: []
143
157
  extensions: []
144
158
  extra_rdoc_files: []
145
159
  files:
160
+ - ".github/workflows/test.yml"
146
161
  - ".gitignore"
147
- - ".travis.yml"
148
162
  - Gemfile
149
163
  - History.txt
150
164
  - LICENSE.txt
@@ -176,12 +190,6 @@ files:
176
190
  - test/decrypted_part_test.rb
177
191
  - test/encrypted_part_test.rb
178
192
  - test/gpg_test.rb
179
- - test/gpghome/gpg-agent.conf
180
- - test/gpghome/pubring.gpg
181
- - test/gpghome/pubring.gpg~
182
- - test/gpghome/random_seed
183
- - test/gpghome/secring.gpg
184
- - test/gpghome/trustdb.gpg
185
193
  - test/gpgme_helper_test.rb
186
194
  - test/hkp_test.rb
187
195
  - test/inline_decrypted_message_test.rb
@@ -194,7 +202,7 @@ homepage: https://github.com/jkraemer/mail-gpg
194
202
  licenses:
195
203
  - MIT
196
204
  metadata: {}
197
- post_install_message:
205
+ post_install_message:
198
206
  rdoc_options: []
199
207
  require_paths:
200
208
  - lib
@@ -209,9 +217,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
209
217
  - !ruby/object:Gem::Version
210
218
  version: '0'
211
219
  requirements: []
212
- rubyforge_project:
213
- rubygems_version: 2.6.14.4
214
- signing_key:
220
+ rubygems_version: 3.5.22
221
+ signing_key:
215
222
  specification_version: 4
216
223
  summary: GPG/MIME encryption plugin for the Ruby Mail Library
217
224
  test_files:
@@ -219,12 +226,6 @@ test_files:
219
226
  - test/decrypted_part_test.rb
220
227
  - test/encrypted_part_test.rb
221
228
  - test/gpg_test.rb
222
- - test/gpghome/gpg-agent.conf
223
- - test/gpghome/pubring.gpg
224
- - test/gpghome/pubring.gpg~
225
- - test/gpghome/random_seed
226
- - test/gpghome/secring.gpg
227
- - test/gpghome/trustdb.gpg
228
229
  - test/gpgme_helper_test.rb
229
230
  - test/hkp_test.rb
230
231
  - test/inline_decrypted_message_test.rb
data/.travis.yml DELETED
@@ -1,21 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.6.3
4
- - 2.5.5
5
- - 2.4.6
6
- env:
7
- - RAILS=4.2.11.1 GPG_BIN=/usr/bin/gpg2
8
- - RAILS=4.2.11.1 GPG_BIN=/usr/bin/gpg
9
- - RAILS=5.2.3 GPG_BIN=/usr/bin/gpg2
10
- - RAILS=5.2.3 GPG_BIN=/usr/bin/gpg
11
- matrix:
12
- exclude:
13
- before_install:
14
- - gem install bundler -v "~> 2.0"
15
- - sudo apt install -y gnupg gnupg2
16
- cache: bundler
17
- dist: xenial
18
- addons:
19
- apt:
20
- update: true
21
-
@@ -1,2 +0,0 @@
1
- allow-preset-passphrase
2
- batch
Binary file
Binary file
Binary file
Binary file
Binary file