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.
- data.tar.gz.sig +1 -0
- data/CONTRIBUTORS +25 -12
- data/History.txt +6 -1
- data/README.md +70 -0
- data/ReleaseNotes +5 -0
- data/bin/sup +22 -15
- data/bin/sup-add +3 -3
- data/bin/sup-config +3 -4
- data/bin/sup-import-dump +1 -1
- data/bin/sup-sync +1 -1
- data/bin/sup-sync-back +2 -2
- data/bin/sup-tweak-labels +1 -1
- data/lib/sup.rb +39 -23
- data/lib/sup/account.rb +4 -0
- data/lib/sup/buffer.rb +4 -7
- data/lib/sup/colormap.rb +10 -2
- data/lib/sup/contact.rb +11 -5
- data/lib/sup/crypto.rb +278 -101
- data/lib/sup/draft.rb +3 -2
- data/lib/sup/horizontal-selector.rb +5 -2
- data/lib/sup/index.rb +47 -42
- data/lib/sup/label.rb +1 -1
- data/lib/sup/message-chunks.rb +4 -2
- data/lib/sup/message.rb +14 -3
- data/lib/sup/modes/buffer-list-mode.rb +1 -1
- data/lib/sup/modes/compose-mode.rb +1 -1
- data/lib/sup/modes/contact-list-mode.rb +2 -2
- data/lib/sup/modes/edit-message-async-mode.rb +109 -0
- data/lib/sup/modes/edit-message-mode.rb +148 -16
- data/lib/sup/modes/file-browser-mode.rb +2 -2
- data/lib/sup/modes/forward-mode.rb +4 -4
- data/lib/sup/modes/line-cursor-mode.rb +2 -2
- data/lib/sup/modes/reply-mode.rb +34 -30
- data/lib/sup/modes/resume-mode.rb +4 -1
- data/lib/sup/modes/scroll-mode.rb +8 -6
- data/lib/sup/modes/text-mode.rb +1 -1
- data/lib/sup/modes/thread-index-mode.rb +44 -25
- data/lib/sup/modes/thread-view-mode.rb +26 -24
- data/lib/sup/person.rb +18 -7
- data/lib/sup/poll.rb +1 -1
- data/lib/sup/rfc2047.rb +1 -1
- data/lib/sup/sent.rb +2 -2
- data/lib/sup/source.rb +1 -1
- data/lib/sup/textfield.rb +38 -1
- data/lib/sup/thread.rb +1 -1
- data/lib/sup/time.rb +83 -0
- data/lib/sup/util.rb +38 -74
- data/lib/sup/version.rb +3 -0
- metadata +333 -168
- metadata.gz.sig +0 -0
- data/README.txt +0 -128
- data/bin/sup-cmd +0 -138
- data/bin/sup-server +0 -44
- data/lib/sup/client.rb +0 -92
- data/lib/sup/protocol.rb +0 -161
- data/lib/sup/server.rb +0 -116
data/lib/sup/account.rb
CHANGED
data/lib/sup/buffer.rb
CHANGED
@@ -1,11 +1,7 @@
|
|
1
1
|
require 'etc'
|
2
2
|
require 'thread'
|
3
3
|
|
4
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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-
|
15
|
-
Runs before gpg is
|
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
|
-
|
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
|
-
|
52
|
+
recipients: an array of recipients of the current email
|
21
53
|
|
22
|
-
Return value:
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
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?;
|
112
|
+
def have_crypto?; @not_working_reason.nil? end
|
40
113
|
|
41
114
|
def sign from, to, payload
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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 =
|
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?
|
103
|
-
|
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
|
106
|
-
|
107
|
-
|
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
|
220
|
+
Chunk::CryptoNotice.new(:valid_untrusted, summary_line, all_output_lines)
|
110
221
|
end
|
111
|
-
elsif
|
112
|
-
|
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
|
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(
|
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
|
-
|
124
|
-
|
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
|
-
|
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(
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
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.
|
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
|
211
|
-
|
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 =
|
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 =
|
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
|