sup 0.12.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (56) hide show
  1. data.tar.gz.sig +1 -0
  2. data/CONTRIBUTORS +25 -12
  3. data/History.txt +6 -1
  4. data/README.md +70 -0
  5. data/ReleaseNotes +5 -0
  6. data/bin/sup +22 -15
  7. data/bin/sup-add +3 -3
  8. data/bin/sup-config +3 -4
  9. data/bin/sup-import-dump +1 -1
  10. data/bin/sup-sync +1 -1
  11. data/bin/sup-sync-back +2 -2
  12. data/bin/sup-tweak-labels +1 -1
  13. data/lib/sup.rb +39 -23
  14. data/lib/sup/account.rb +4 -0
  15. data/lib/sup/buffer.rb +4 -7
  16. data/lib/sup/colormap.rb +10 -2
  17. data/lib/sup/contact.rb +11 -5
  18. data/lib/sup/crypto.rb +278 -101
  19. data/lib/sup/draft.rb +3 -2
  20. data/lib/sup/horizontal-selector.rb +5 -2
  21. data/lib/sup/index.rb +47 -42
  22. data/lib/sup/label.rb +1 -1
  23. data/lib/sup/message-chunks.rb +4 -2
  24. data/lib/sup/message.rb +14 -3
  25. data/lib/sup/modes/buffer-list-mode.rb +1 -1
  26. data/lib/sup/modes/compose-mode.rb +1 -1
  27. data/lib/sup/modes/contact-list-mode.rb +2 -2
  28. data/lib/sup/modes/edit-message-async-mode.rb +109 -0
  29. data/lib/sup/modes/edit-message-mode.rb +148 -16
  30. data/lib/sup/modes/file-browser-mode.rb +2 -2
  31. data/lib/sup/modes/forward-mode.rb +4 -4
  32. data/lib/sup/modes/line-cursor-mode.rb +2 -2
  33. data/lib/sup/modes/reply-mode.rb +34 -30
  34. data/lib/sup/modes/resume-mode.rb +4 -1
  35. data/lib/sup/modes/scroll-mode.rb +8 -6
  36. data/lib/sup/modes/text-mode.rb +1 -1
  37. data/lib/sup/modes/thread-index-mode.rb +44 -25
  38. data/lib/sup/modes/thread-view-mode.rb +26 -24
  39. data/lib/sup/person.rb +18 -7
  40. data/lib/sup/poll.rb +1 -1
  41. data/lib/sup/rfc2047.rb +1 -1
  42. data/lib/sup/sent.rb +2 -2
  43. data/lib/sup/source.rb +1 -1
  44. data/lib/sup/textfield.rb +38 -1
  45. data/lib/sup/thread.rb +1 -1
  46. data/lib/sup/time.rb +83 -0
  47. data/lib/sup/util.rb +38 -74
  48. data/lib/sup/version.rb +3 -0
  49. metadata +333 -168
  50. metadata.gz.sig +0 -0
  51. data/README.txt +0 -128
  52. data/bin/sup-cmd +0 -138
  53. data/bin/sup-server +0 -44
  54. data/lib/sup/client.rb +0 -92
  55. data/lib/sup/protocol.rb +0 -161
  56. data/lib/sup/server.rb +0 -116
data/lib/sup/account.rb CHANGED
@@ -80,6 +80,10 @@ class AccountManager
80
80
  @regexen.argfind { |re, a| re =~ email && a }
81
81
  end
82
82
  end
83
+ def full_address_for email
84
+ a = account_for email
85
+ Person.full_address a.name, email
86
+ end
83
87
  end
84
88
 
85
89
  end
data/lib/sup/buffer.rb CHANGED
@@ -1,11 +1,7 @@
1
1
  require 'etc'
2
2
  require 'thread'
3
3
 
4
- begin
5
- require 'ncursesw'
6
- rescue LoadError
7
- require 'ncurses'
8
- end
4
+ require 'ncursesw'
9
5
 
10
6
  if defined? Ncurses
11
7
  module Ncurses
@@ -73,7 +69,7 @@ class InputSequenceAborted < StandardError; end
73
69
  class Buffer
74
70
  attr_reader :mode, :x, :y, :width, :height, :title, :atime
75
71
  bool_reader :dirty, :system
76
- bool_accessor :force_to_top
72
+ bool_accessor :force_to_top, :hidden
77
73
 
78
74
  def initialize window, mode, width, height, opts={}
79
75
  @w = window
@@ -82,6 +78,7 @@ class Buffer
82
78
  @focus = false
83
79
  @title = opts[:title] || ""
84
80
  @force_to_top = opts[:force_to_top] || false
81
+ @hidden = opts[:hidden] || false
85
82
  @x, @y, @width, @height = 0, 0, width, height
86
83
  @atime = Time.at 0
87
84
  @system = opts[:system] || false
@@ -265,7 +262,7 @@ EOS
265
262
  end
266
263
 
267
264
  def rollable_buffers
268
- @buffers.select { |b| !b.system? || @buffers.last == b }
265
+ @buffers.select { |b| !(b.system? || b.hidden?) || @buffers.last == b }
269
266
  end
270
267
 
271
268
  def handle_input c
data/lib/sup/colormap.rb CHANGED
@@ -40,6 +40,7 @@ class Colormap
40
40
  :missing_message => { :fg => "black", :bg => "red" },
41
41
  :attachment => { :fg => "cyan", :bg => "default" },
42
42
  :cryptosig_valid => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
43
+ :cryptosig_valid_untrusted => { :fg => "yellow", :bg => "blue", :attrs => ["bold"] },
43
44
  :cryptosig_unknown => { :fg => "cyan", :bg => "default" },
44
45
  :cryptosig_invalid => { :fg => "yellow", :bg => "red", :attrs => ["bold"] },
45
46
  :generic_notice_patina => { :fg => "cyan", :bg => "default" },
@@ -63,8 +64,9 @@ class Colormap
63
64
  :regular_buf => { :fg => "white", :bg => "default" },
64
65
  :modified_buffer => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
65
66
  :date => { :fg => "white", :bg => "default"},
67
+ :size_widget => { :fg => "white", :bg => "default"},
66
68
  }
67
-
69
+
68
70
  def initialize
69
71
  raise "only one instance can be created" if @@instance
70
72
  @@instance = self
@@ -114,7 +116,7 @@ class Colormap
114
116
  Curses::COLOR_BLACK
115
117
  end
116
118
 
117
- hbg =
119
+ hbg =
118
120
  case bg
119
121
  when Curses::COLOR_CYAN
120
122
  Curses::COLOR_YELLOW
@@ -175,6 +177,10 @@ class Colormap
175
177
  color
176
178
  end
177
179
 
180
+ def sym_is_defined sym
181
+ return sym if @entries.member? sym
182
+ end
183
+
178
184
  ## Try to use the user defined colors, in case of an error fall back
179
185
  ## to the default ones.
180
186
  def populate_colormap
@@ -219,6 +225,8 @@ class Colormap
219
225
  Colormap.new unless @@instance
220
226
  @@instance.send meth, *a
221
227
  end
228
+ # Performance shortcut
229
+ def self.color_for *a; @@instance.color_for *a; end
222
230
  end
223
231
 
224
232
  end
data/lib/sup/contact.rb CHANGED
@@ -12,14 +12,13 @@ class ContactManager
12
12
 
13
13
  @p2a = {} # person to alias
14
14
  @a2p = {} # alias to person
15
+ @e2p = {} # email to person
15
16
 
16
17
  if File.exists? fn
17
18
  IO.foreach(fn) do |l|
18
19
  l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
19
20
  aalias, addr = $1, $2
20
- p = Person.from_address addr
21
- @p2a[p] = aalias
22
- @a2p[aalias] = p unless aalias.nil? || aalias.empty?
21
+ update_alias Person.from_address(addr), aalias
23
22
  end
24
23
  end
25
24
  end
@@ -28,11 +27,16 @@ class ContactManager
28
27
  def contacts_with_aliases; @a2p.values.uniq end
29
28
 
30
29
  def update_alias person, aalias=nil
31
- if(old_aalias = @p2a[person]) # remove old alias
30
+ old_aalias = @p2a[person]
31
+ if(old_aalias != nil and old_aalias != "") # remove old alias
32
32
  @a2p.delete old_aalias
33
+ @e2p.delete person.email
33
34
  end
34
35
  @p2a[person] = aalias
35
- @a2p[aalias] = person unless aalias.nil? || aalias.empty?
36
+ unless aalias.nil? || aalias.empty?
37
+ @a2p[aalias] = person
38
+ @e2p[person.email] = person
39
+ end
36
40
  end
37
41
 
38
42
  ## this may not actually be called anywhere, since we still keep contacts
@@ -40,11 +44,13 @@ class ContactManager
40
44
  def drop_contact person
41
45
  aalias = @p2a[person]
42
46
  @p2a.delete person
47
+ @e2p.delete person.email
43
48
  @a2p.delete aalias if aalias
44
49
  end
45
50
 
46
51
  def contact_for aalias; @a2p[aalias] end
47
52
  def alias_for person; @p2a[person] end
53
+ def person_for email; @e2p[email] end
48
54
  def is_aliased_contact? person; !@p2a[person].nil? end
49
55
 
50
56
  def save
data/lib/sup/crypto.rb CHANGED
@@ -1,3 +1,10 @@
1
+ begin
2
+ # gpgme broke its API in 2.0, so make sure we have the old version for now.
3
+ gem 'gpgme', '=1.0.8'
4
+ require 'gpgme'
5
+ rescue LoadError
6
+ end
7
+
1
8
  module Redwood
2
9
 
3
10
  class CryptoManager
@@ -11,76 +18,156 @@ class CryptoManager
11
18
  [:encrypt, "Encrypt only"]
12
19
  )
13
20
 
14
- HookManager.register "gpg-args", <<EOS
15
- Runs before gpg is executed, allowing you to modify the arguments (most
21
+ HookManager.register "gpg-options", <<EOS
22
+ Runs before gpg is called, allowing you to modify the options (most
16
23
  likely you would want to add something to certain commands, like
17
- --trust-model always to signing/encrypting a message, but who knows).
24
+ {:always_trust => true} to encrypting a message, but who knows).
25
+
26
+ Variables:
27
+ operation: what operation will be done ("sign", "encrypt", "decrypt" or "verify")
28
+ options: a dictionary of values to be passed to GPGME
29
+
30
+ Return value: a dictionary to be passed to GPGME
31
+ EOS
32
+
33
+ HookManager.register "sig-output", <<EOS
34
+ Runs when the signature output is being generated, allowing you to
35
+ add extra information to your signatures if you want.
36
+
37
+ Variables:
38
+ signature: the signature object (class is GPGME::Signature)
39
+ from_key: the key that generated the signature (class is GPGME::Key)
40
+
41
+ Return value: an array of lines of output
42
+ EOS
43
+
44
+ HookManager.register "gpg-expand-keys", <<EOS
45
+ Runs when the list of encryption recipients is created, allowing you to
46
+ replace a recipient with one or more GPGME recipients. For example, you could
47
+ replace the email address of a mailing list with the key IDs that belong to
48
+ the recipients of that list. This is essentially what GPG groups do, which
49
+ are not supported by GPGME.
18
50
 
19
51
  Variables:
20
- args: arguments for running GPG
52
+ recipients: an array of recipients of the current email
21
53
 
22
- Return value: the arguments for running GPG
54
+ Return value: an array of recipients (email address or GPG key ID) to encrypt
55
+ the email for
23
56
  EOS
24
57
 
25
58
  def initialize
26
59
  @mutex = Mutex.new
27
60
 
28
- bin = `which gpg`.chomp
29
- @cmd = case bin
30
- when /\S/
31
- debug "crypto: detected gpg binary in #{bin}"
32
- "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
61
+ @not_working_reason = nil
62
+
63
+ # test if the gpgme gem is available
64
+ @gpgme_present =
65
+ begin
66
+ begin
67
+ GPGME.check_version({:protocol => GPGME::PROTOCOL_OpenPGP})
68
+ true
69
+ rescue GPGME::Error
70
+ false
71
+ end
72
+ rescue NameError
73
+ false
74
+ end
75
+
76
+ unless @gpgme_present
77
+ @not_working_reason = ['gpgme gem not present',
78
+ 'Install the gpgme gem in order to use signed and encrypted emails']
79
+ return
80
+ end
81
+
82
+ # if gpg2 is available, it will start gpg-agent if required
83
+ if (bin = `which gpg2`.chomp) =~ /\S/
84
+ GPGME.set_engine_info GPGME::PROTOCOL_OpenPGP, bin, nil
33
85
  else
34
- debug "crypto: no gpg binary detected"
35
- nil
86
+ # check if the gpg-options hook uses the passphrase_callback
87
+ # if it doesn't then check if gpg agent is present
88
+ gpg_opts = HookManager.run("gpg-options",
89
+ {:operation => "sign", :options => {}}) || {}
90
+ if gpg_opts[:passphrase_callback].nil?
91
+ if ENV['GPG_AGENT_INFO'].nil?
92
+ @not_working_reason = ["Environment variable 'GPG_AGENT_INFO' not set, is gpg-agent running?",
93
+ "If gpg-agent is running, try $ export `cat ~/.gpg-agent-info`"]
94
+ return
95
+ end
96
+
97
+ gpg_agent_socket_file = ENV['GPG_AGENT_INFO'].split(':')[0]
98
+ unless File.exist?(gpg_agent_socket_file)
99
+ @not_working_reason = ["gpg-agent socket file #{gpg_agent_socket_file} does not exist"]
100
+ return
101
+ end
102
+
103
+ s = File.stat(gpg_agent_socket_file)
104
+ unless s.socket?
105
+ @not_working_reason = ["gpg-agent socket file #{gpg_agent_socket_file} is not a socket"]
106
+ return
107
+ end
108
+ end
36
109
  end
37
110
  end
38
111
 
39
- def have_crypto?; !@cmd.nil? end
112
+ def have_crypto?; @not_working_reason.nil? end
40
113
 
41
114
  def sign from, to, payload
42
- payload_fn = Tempfile.new "redwood.payload"
43
- payload_fn.write format_payload(payload)
44
- payload_fn.close
115
+ return unknown_status(@not_working_reason) unless @not_working_reason.nil?
116
+
117
+ gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
118
+ gpg_opts.merge!(gen_sign_user_opts(from))
119
+ gpg_opts = HookManager.run("gpg-options",
120
+ {:operation => "sign", :options => gpg_opts}) || gpg_opts
45
121
 
46
- sig_fn = Tempfile.new "redwood.signature"; sig_fn.close
122
+ begin
123
+ sig = GPGME.detach_sign(format_payload(payload), gpg_opts)
124
+ rescue GPGME::Error => exc
125
+ raise Error, gpgme_exc_msg(exc.message)
126
+ end
47
127
 
48
- sign_user_opts = gen_sign_user_opts from
49
- message = run_gpg "--output #{sig_fn.path} --yes --armor --detach-sign --textmode --digest-algo sha256 #{sign_user_opts} #{payload_fn.path}", :interactive => true
50
- unless $?.success?
51
- info "Error while running gpg: #{message}"
52
- raise Error, "GPG command failed. See log for details."
128
+ # if the key (or gpg-agent) is not available GPGME does not complain
129
+ # but just returns a zero length string. Let's catch that
130
+ if sig.length == 0
131
+ raise Error, gpgme_exc_msg("GPG failed to generate signature: check that gpg-agent is running and your key is available.")
53
132
  end
54
133
 
55
134
  envelope = RMail::Message.new
56
- envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha256'
135
+ envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature'
57
136
 
58
137
  envelope.add_part payload
59
- signature = RMail::Message.make_attachment IO.read(sig_fn.path), "application/pgp-signature", nil, "signature.asc"
138
+ signature = RMail::Message.make_attachment sig, "application/pgp-signature", nil, "signature.asc"
60
139
  envelope.add_part signature
61
140
  envelope
62
141
  end
63
142
 
64
143
  def encrypt from, to, payload, sign=false
65
- payload_fn = Tempfile.new "redwood.payload"
66
- payload_fn.write format_payload(payload)
67
- payload_fn.close
144
+ return unknown_status(@not_working_reason) unless @not_working_reason.nil?
68
145
 
69
- encrypted_fn = Tempfile.new "redwood.encrypted"; encrypted_fn.close
146
+ gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
147
+ if sign
148
+ gpg_opts.merge!(gen_sign_user_opts(from))
149
+ gpg_opts.merge!({:sign => true})
150
+ end
151
+ gpg_opts = HookManager.run("gpg-options",
152
+ {:operation => "encrypt", :options => gpg_opts}) || gpg_opts
153
+ recipients = to + [from]
154
+ recipients = HookManager.run("gpg-expand-keys", { :recipients => recipients }) || recipients
155
+ begin
156
+ cipher = GPGME.encrypt(recipients, format_payload(payload), gpg_opts)
157
+ rescue GPGME::Error => exc
158
+ raise Error, gpgme_exc_msg(exc.message)
159
+ end
70
160
 
71
- recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ")
72
- sign_opts = ""
73
- sign_opts = "--sign --digest-algo sha256 " + gen_sign_user_opts(from) if sign
74
- message = run_gpg "--output #{encrypted_fn.path} --yes --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}", :interactive => true
75
- unless $?.success?
76
- info "Error while running gpg: #{message}"
77
- raise Error, "GPG command failed. See log for details."
161
+ # if the key (or gpg-agent) is not available GPGME does not complain
162
+ # but just returns a zero length string. Let's catch that
163
+ if cipher.length == 0
164
+ raise Error, gpgme_exc_msg("GPG failed to generate cipher text: check that gpg-agent is running and your key is available.")
78
165
  end
79
166
 
80
167
  encrypted_payload = RMail::Message.new
81
168
  encrypted_payload.header["Content-Type"] = "application/octet-stream"
82
169
  encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"'
83
- encrypted_payload.body = IO.read(encrypted_fn.path)
170
+ encrypted_payload.body = cipher
84
171
 
85
172
  control = RMail::Message.new
86
173
  control.header["Content-Type"] = "application/pgp-encrypted"
@@ -99,70 +186,104 @@ EOS
99
186
  encrypt from, to, payload, true
100
187
  end
101
188
 
102
- def verified_ok? output, rc
103
- output_lines = output.split(/\n/)
189
+ def verified_ok? verify_result
190
+ valid = true
191
+ unknown = false
192
+ all_output_lines = []
193
+ all_trusted = true
194
+
195
+ verify_result.signatures.each do |signature|
196
+ output_lines, trusted = sig_output_lines signature
197
+ all_output_lines << output_lines
198
+ all_output_lines.flatten!
199
+ all_trusted &&= trusted
200
+
201
+ err_code = GPGME::gpgme_err_code(signature.status)
202
+ if err_code == GPGME::GPG_ERR_BAD_SIGNATURE
203
+ valid = false
204
+ elsif err_code != GPGME::GPG_ERR_NO_ERROR
205
+ valid = false
206
+ unknown = true
207
+ end
208
+ end
104
209
 
105
- if output =~ /^gpg: (.* signature from .*$)/
106
- if rc == 0
107
- Chunk::CryptoNotice.new :valid, $1, output_lines
210
+ if valid || !unknown
211
+ summary_line = simplify_sig_line(verify_result.signatures[0].to_s, all_trusted)
212
+ end
213
+
214
+ if all_output_lines.length == 0
215
+ Chunk::CryptoNotice.new :valid, "Encrypted message wasn't signed", all_output_lines
216
+ elsif valid
217
+ if all_trusted
218
+ Chunk::CryptoNotice.new(:valid, summary_line, all_output_lines)
108
219
  else
109
- Chunk::CryptoNotice.new :invalid, $1, output_lines
220
+ Chunk::CryptoNotice.new(:valid_untrusted, summary_line, all_output_lines)
110
221
  end
111
- elsif output_lines.length == 0 && rc == 0
112
- # the message wasn't signed
113
- Chunk::CryptoNotice.new :valid, "Encrypted message wasn't signed", output_lines
222
+ elsif !unknown
223
+ Chunk::CryptoNotice.new(:invalid, summary_line, all_output_lines)
114
224
  else
115
- unknown_status output_lines
225
+ unknown_status all_output_lines
116
226
  end
117
227
  end
118
228
 
119
229
  def verify payload, signature, detached=true # both RubyMail::Message objects
120
- return unknown_status(cant_find_binary) unless @cmd
230
+ return unknown_status(@not_working_reason) unless @not_working_reason.nil?
121
231
 
232
+ gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP}
233
+ gpg_opts = HookManager.run("gpg-options",
234
+ {:operation => "verify", :options => gpg_opts}) || gpg_opts
235
+ ctx = GPGME::Ctx.new(gpg_opts)
236
+ sig_data = GPGME::Data.from_str signature.decode
122
237
  if detached
123
- payload_fn = Tempfile.new "redwood.payload"
124
- payload_fn.write format_payload(payload)
125
- payload_fn.close
126
- end
127
-
128
- signature_fn = Tempfile.new "redwood.signature"
129
- signature_fn.write signature.decode
130
- signature_fn.close
131
-
132
- if detached
133
- output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
238
+ signed_text_data = GPGME::Data.from_str(format_payload(payload))
239
+ plain_data = nil
134
240
  else
135
- output = run_gpg "--verify #{signature_fn.path}"
241
+ signed_text_data = nil
242
+ plain_data = GPGME::Data.empty
243
+ end
244
+ begin
245
+ ctx.verify(sig_data, signed_text_data, plain_data)
246
+ rescue GPGME::Error => exc
247
+ return unknown_status [gpgme_exc_msg(exc.message)]
248
+ end
249
+ begin
250
+ self.verified_ok? ctx.verify_result
251
+ rescue ArgumentError => exc
252
+ return unknown_status [gpgme_exc_msg(exc.message)]
136
253
  end
137
-
138
- self.verified_ok? output, $?
139
254
  end
140
255
 
141
256
  ## returns decrypted_message, status, desc, lines
142
257
  def decrypt payload, armor=false # a RubyMail::Message object
143
- return unknown_status(cant_find_binary) unless @cmd
144
-
145
- payload_fn = Tempfile.new(["redwood.payload", ".asc"])
146
- payload_fn.write payload.to_s
147
- payload_fn.close
148
-
149
- output_fn = Tempfile.new "redwood.output"
150
- output_fn.close
151
-
152
- message = run_gpg "--output #{output_fn.path} --skip-verify --yes --decrypt #{payload_fn.path}", :interactive => true
153
-
154
- unless $?.success?
155
- info "Error while running gpg: #{message}"
156
- return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", message.split("\n"))
258
+ return unknown_status(@not_working_reason) unless @not_working_reason.nil?
259
+
260
+ gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP}
261
+ gpg_opts = HookManager.run("gpg-options",
262
+ {:operation => "decrypt", :options => gpg_opts}) || gpg_opts
263
+ ctx = GPGME::Ctx.new(gpg_opts)
264
+ cipher_data = GPGME::Data.from_str(format_payload(payload))
265
+ plain_data = GPGME::Data.empty
266
+ begin
267
+ ctx.decrypt_verify(cipher_data, plain_data)
268
+ rescue GPGME::Error => exc
269
+ return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", gpgme_exc_msg(exc.message))
157
270
  end
158
-
159
- output = IO.read output_fn.path
271
+ begin
272
+ sig = self.verified_ok? ctx.verify_result
273
+ rescue ArgumentError => exc
274
+ sig = unknown_status [gpgme_exc_msg(exc.message)]
275
+ end
276
+ plain_data.seek(0, IO::SEEK_SET)
277
+ output = plain_data.read
160
278
  output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding
161
279
 
280
+ ## TODO: test to see if it is still necessary to do a 2nd run if verify
281
+ ## fails.
282
+ #
162
283
  ## check for a valid signature in an extra run because gpg aborts if the
163
284
  ## signature cannot be verified (but it is still able to decrypt)
164
- sigoutput = run_gpg "#{payload_fn.path}"
165
- sig = self.verified_ok? sigoutput, $?
285
+ #sigoutput = run_gpg "#{payload_fn.path}"
286
+ #sig = self.old_verified_ok? sigoutput, $?
166
287
 
167
288
  if armor
168
289
  msg = RMail::Message.new
@@ -174,7 +295,7 @@ EOS
174
295
  msg.body = output
175
296
  else
176
297
  # It appears that some clients use Windows new lines - CRLF - but RMail
177
- # splits the body and header on "\n\n". So to allow the parse below to
298
+ # splits the body and header on "\n\n". So to allow the parse below to
178
299
  # succeed, we will convert the newlines to what RMail expects
179
300
  output = output.gsub(/\r\n/, "\n")
180
301
  # This is gross. This decrypted payload could very well be a multipart
@@ -207,8 +328,10 @@ private
207
328
  Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
208
329
  end
209
330
 
210
- def cant_find_binary
211
- ["Can't find gpg binary in path."]
331
+ def gpgme_exc_msg msg
332
+ err_msg = "Exception in GPGME call: #{msg}"
333
+ info err_msg
334
+ err_msg
212
335
  end
213
336
 
214
337
  ## here's where we munge rmail output into the format that signed/encrypted
@@ -217,37 +340,91 @@ private
217
340
  payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n")
218
341
  end
219
342
 
343
+ # remove the hex key_id and info in ()
344
+ def simplify_sig_line sig_line, trusted
345
+ sig_line.sub!(/from [0-9A-F]{16} /, "from ")
346
+ if !trusted
347
+ sig_line.sub!(/Good signature/, "Good (untrusted) signature")
348
+ end
349
+ sig_line
350
+ end
351
+
352
+ def sig_output_lines signature
353
+ # It appears that the signature.to_s call can lead to a EOFError if
354
+ # the key is not found. So start by looking for the key.
355
+ ctx = GPGME::Ctx.new
356
+ begin
357
+ from_key = ctx.get_key(signature.fingerprint)
358
+ if GPGME::gpgme_err_code(signature.status) == GPGME::GPG_ERR_GENERAL
359
+ first_sig = "General error on signature verification for #{signature.fingerprint}"
360
+ elsif signature.to_s
361
+ first_sig = signature.to_s.sub(/from [0-9A-F]{16} /, 'from "') + '"'
362
+ else
363
+ first_sig = "Unknown error or empty signature"
364
+ end
365
+ rescue EOFError
366
+ from_key = nil
367
+ first_sig = "No public key available for #{signature.fingerprint}"
368
+ end
369
+
370
+ time_line = "Signature made " + signature.timestamp.strftime("%a %d %b %Y %H:%M:%S %Z") +
371
+ " using " + key_type(from_key, signature.fingerprint) +
372
+ "key ID " + signature.fingerprint[-8..-1]
373
+ output_lines = [time_line, first_sig]
374
+
375
+ trusted = false
376
+ if from_key
377
+ # first list all the uids
378
+ if from_key.uids.length > 1
379
+ aka_list = from_key.uids[1..-1]
380
+ aka_list.each { |aka| output_lines << ' aka "' + aka.uid + '"' }
381
+ end
382
+
383
+ # now we want to look at the trust of that key
384
+ if signature.validity != GPGME::GPGME_VALIDITY_FULL && signature.validity != GPGME::GPGME_VALIDITY_MARGINAL
385
+ output_lines << "WARNING: This key is not certified with a trusted signature!"
386
+ output_lines << "There is no indication that the signature belongs to the owner"
387
+ output_lines << "Full fingerprint is: " + (0..9).map {|i| signature.fpr[(i*4),4]}.join(":")
388
+ else
389
+ trusted = true
390
+ end
391
+
392
+ # finally, run the hook
393
+ output_lines << HookManager.run("sig-output",
394
+ {:signature => signature, :from_key => from_key})
395
+ end
396
+ return output_lines, trusted
397
+ end
398
+
399
+ def key_type key, fpr
400
+ return "" if key.nil?
401
+ subkey = key.subkeys.find {|subkey| subkey.fpr == fpr || subkey.keyid == fpr }
402
+ return "" if subkey.nil?
403
+
404
+ case subkey.pubkey_algo
405
+ when GPGME::PK_RSA then "RSA "
406
+ when GPGME::PK_DSA then "DSA "
407
+ when GPGME::PK_ELG then "ElGamel "
408
+ when GPGME::PK_ELG_E then "ElGamel "
409
+ end
410
+ end
411
+
220
412
  # logic is:
221
413
  # if gpgkey set for this account, then use that
222
414
  # elsif only one account, then leave blank so gpg default will be user
223
415
  # else set --local-user from_email_address
224
416
  def gen_sign_user_opts from
225
417
  account = AccountManager.account_for from
418
+ account ||= AccountManager.default_account
226
419
  if !account.gpgkey.nil?
227
- opts = "--local-user '#{account.gpgkey}'"
420
+ opts = {:signers => account.gpgkey}
228
421
  elsif AccountManager.user_emails.length == 1
229
422
  # only one account
230
- opts = ""
423
+ opts = {}
231
424
  else
232
- opts = "--local-user '#{from}'"
425
+ opts = {:signers => from}
233
426
  end
234
427
  opts
235
428
  end
236
-
237
- def run_gpg args, opts={}
238
- args = HookManager.run("gpg-args", { :args => args }) || args
239
- cmd = "LC_MESSAGES=C #{@cmd} #{args}"
240
- if opts[:interactive] && BufferManager.instantiated?
241
- output_fn = Tempfile.new "redwood.output"
242
- output_fn.close
243
- cmd += " > #{output_fn.path} 2> /dev/null"
244
- debug "crypto: running: #{cmd}"
245
- BufferManager.shell_out cmd
246
- IO.read(output_fn.path) rescue "can't read output"
247
- else
248
- debug "crypto: running: #{cmd}"
249
- `#{cmd} 2> /dev/null`
250
- end
251
- end
252
429
  end
253
430
  end