sup 0.19.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTORS +84 -0
- data/Gemfile +3 -0
- data/HACKING +42 -0
- data/History.txt +361 -0
- data/LICENSE +280 -0
- data/README.md +70 -0
- data/Rakefile +12 -0
- data/ReleaseNotes +231 -0
- data/bin/sup +434 -0
- data/bin/sup-add +118 -0
- data/bin/sup-config +243 -0
- data/bin/sup-dump +43 -0
- data/bin/sup-import-dump +101 -0
- data/bin/sup-psych-ify-config-files +21 -0
- data/bin/sup-recover-sources +87 -0
- data/bin/sup-sync +210 -0
- data/bin/sup-sync-back-maildir +127 -0
- data/bin/sup-tweak-labels +140 -0
- data/contrib/colorpicker.rb +100 -0
- data/contrib/completion/_sup.zsh +114 -0
- data/devel/console.sh +3 -0
- data/devel/count-loc.sh +3 -0
- data/devel/load-index.rb +9 -0
- data/devel/profile.rb +12 -0
- data/devel/start-console.rb +5 -0
- data/doc/FAQ.txt +119 -0
- data/doc/Hooks.txt +79 -0
- data/doc/Philosophy.txt +69 -0
- data/lib/sup.rb +467 -0
- data/lib/sup/account.rb +90 -0
- data/lib/sup/buffer.rb +768 -0
- data/lib/sup/colormap.rb +239 -0
- data/lib/sup/contact.rb +67 -0
- data/lib/sup/crypto.rb +461 -0
- data/lib/sup/draft.rb +119 -0
- data/lib/sup/hook.rb +159 -0
- data/lib/sup/horizontal_selector.rb +59 -0
- data/lib/sup/idle.rb +42 -0
- data/lib/sup/index.rb +882 -0
- data/lib/sup/interactive_lock.rb +89 -0
- data/lib/sup/keymap.rb +140 -0
- data/lib/sup/label.rb +87 -0
- data/lib/sup/logger.rb +77 -0
- data/lib/sup/logger/singleton.rb +10 -0
- data/lib/sup/maildir.rb +257 -0
- data/lib/sup/mbox.rb +187 -0
- data/lib/sup/message.rb +803 -0
- data/lib/sup/message_chunks.rb +328 -0
- data/lib/sup/mode.rb +140 -0
- data/lib/sup/modes/buffer_list_mode.rb +50 -0
- data/lib/sup/modes/completion_mode.rb +55 -0
- data/lib/sup/modes/compose_mode.rb +38 -0
- data/lib/sup/modes/console_mode.rb +125 -0
- data/lib/sup/modes/contact_list_mode.rb +148 -0
- data/lib/sup/modes/edit_message_async_mode.rb +110 -0
- data/lib/sup/modes/edit_message_mode.rb +728 -0
- data/lib/sup/modes/file_browser_mode.rb +109 -0
- data/lib/sup/modes/forward_mode.rb +82 -0
- data/lib/sup/modes/help_mode.rb +19 -0
- data/lib/sup/modes/inbox_mode.rb +85 -0
- data/lib/sup/modes/label_list_mode.rb +138 -0
- data/lib/sup/modes/label_search_results_mode.rb +38 -0
- data/lib/sup/modes/line_cursor_mode.rb +203 -0
- data/lib/sup/modes/log_mode.rb +57 -0
- data/lib/sup/modes/person_search_results_mode.rb +12 -0
- data/lib/sup/modes/poll_mode.rb +19 -0
- data/lib/sup/modes/reply_mode.rb +228 -0
- data/lib/sup/modes/resume_mode.rb +52 -0
- data/lib/sup/modes/scroll_mode.rb +252 -0
- data/lib/sup/modes/search_list_mode.rb +204 -0
- data/lib/sup/modes/search_results_mode.rb +59 -0
- data/lib/sup/modes/text_mode.rb +76 -0
- data/lib/sup/modes/thread_index_mode.rb +1033 -0
- data/lib/sup/modes/thread_view_mode.rb +941 -0
- data/lib/sup/person.rb +134 -0
- data/lib/sup/poll.rb +272 -0
- data/lib/sup/rfc2047.rb +56 -0
- data/lib/sup/search.rb +110 -0
- data/lib/sup/sent.rb +58 -0
- data/lib/sup/service/label_service.rb +45 -0
- data/lib/sup/source.rb +244 -0
- data/lib/sup/tagger.rb +50 -0
- data/lib/sup/textfield.rb +253 -0
- data/lib/sup/thread.rb +452 -0
- data/lib/sup/time.rb +93 -0
- data/lib/sup/undo.rb +38 -0
- data/lib/sup/update.rb +30 -0
- data/lib/sup/util.rb +747 -0
- data/lib/sup/util/ncurses.rb +274 -0
- data/lib/sup/util/path.rb +9 -0
- data/lib/sup/util/query.rb +17 -0
- data/lib/sup/util/uri.rb +15 -0
- data/lib/sup/version.rb +3 -0
- data/sup.gemspec +53 -0
- data/test/dummy_source.rb +61 -0
- data/test/gnupg_test_home/gpg.conf +1 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
- data/test/gnupg_test_home/trustdb.gpg +0 -0
- data/test/integration/test_label_service.rb +18 -0
- data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
- data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
- data/test/messages/missing-line.eml +9 -0
- data/test/test_crypto.rb +109 -0
- data/test/test_header_parsing.rb +168 -0
- data/test/test_helper.rb +7 -0
- data/test/test_message.rb +532 -0
- data/test/test_messages_dir.rb +147 -0
- data/test/test_yaml_migration.rb +85 -0
- data/test/test_yaml_regressions.rb +17 -0
- data/test/unit/service/test_label_service.rb +19 -0
- data/test/unit/test_horizontal_selector.rb +40 -0
- data/test/unit/util/test_query.rb +46 -0
- data/test/unit/util/test_string.rb +57 -0
- data/test/unit/util/test_uri.rb +19 -0
- metadata +423 -0
data/lib/sup/colormap.rb
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
module Ncurses
|
|
2
|
+
COLOR_DEFAULT = -1
|
|
3
|
+
|
|
4
|
+
NUM_COLORS = `tput colors`.to_i
|
|
5
|
+
MAX_PAIRS = `tput pairs`.to_i
|
|
6
|
+
|
|
7
|
+
def self.color! name, value
|
|
8
|
+
const_set "COLOR_#{name.to_s.upcase}", value
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
## numeric colors
|
|
12
|
+
Ncurses::NUM_COLORS.times { |x| color! x, x }
|
|
13
|
+
|
|
14
|
+
if Ncurses::NUM_COLORS == 256
|
|
15
|
+
## xterm 6x6x6 color cube
|
|
16
|
+
6.times { |x| 6.times { |y| 6.times { |z| color! "c#{x}#{y}#{z}", 16 + z + 6*y + 36*x } } }
|
|
17
|
+
|
|
18
|
+
## xterm 24-shade grayscale
|
|
19
|
+
24.times { |x| color! "g#{x}", (16+6*6*6) + x }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Redwood
|
|
24
|
+
|
|
25
|
+
class Colormap
|
|
26
|
+
@@instance = nil
|
|
27
|
+
|
|
28
|
+
DEFAULT_COLORS = {
|
|
29
|
+
:text => { :fg => "white", :bg => "black" },
|
|
30
|
+
:status => { :fg => "white", :bg => "blue", :attrs => ["bold"] },
|
|
31
|
+
:index_old => { :fg => "white", :bg => "default" },
|
|
32
|
+
:index_new => { :fg => "white", :bg => "default", :attrs => ["bold"] },
|
|
33
|
+
:index_starred => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
|
|
34
|
+
:index_draft => { :fg => "red", :bg => "default", :attrs => ["bold"] },
|
|
35
|
+
:labellist_old => { :fg => "white", :bg => "default" },
|
|
36
|
+
:labellist_new => { :fg => "white", :bg => "default", :attrs => ["bold"] },
|
|
37
|
+
:twiddle => { :fg => "blue", :bg => "default" },
|
|
38
|
+
:label => { :fg => "yellow", :bg => "default" },
|
|
39
|
+
:message_patina => { :fg => "black", :bg => "green" },
|
|
40
|
+
:alternate_patina => { :fg => "black", :bg => "blue" },
|
|
41
|
+
:missing_message => { :fg => "black", :bg => "red" },
|
|
42
|
+
:attachment => { :fg => "cyan", :bg => "default" },
|
|
43
|
+
:cryptosig_valid => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
|
|
44
|
+
:cryptosig_valid_untrusted => { :fg => "yellow", :bg => "blue", :attrs => ["bold"] },
|
|
45
|
+
:cryptosig_unknown => { :fg => "cyan", :bg => "default" },
|
|
46
|
+
:cryptosig_invalid => { :fg => "yellow", :bg => "red", :attrs => ["bold"] },
|
|
47
|
+
:generic_notice_patina => { :fg => "cyan", :bg => "default" },
|
|
48
|
+
:quote_patina => { :fg => "yellow", :bg => "default" },
|
|
49
|
+
:sig_patina => { :fg => "yellow", :bg => "default" },
|
|
50
|
+
:quote => { :fg => "yellow", :bg => "default" },
|
|
51
|
+
:sig => { :fg => "yellow", :bg => "default" },
|
|
52
|
+
:to_me => { :fg => "green", :bg => "default" },
|
|
53
|
+
:with_attachment => { :fg => "green", :bg => "default" },
|
|
54
|
+
:starred => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
|
|
55
|
+
:starred_patina => { :fg => "yellow", :bg => "green", :attrs => ["bold"] },
|
|
56
|
+
:alternate_starred_patina => { :fg => "yellow", :bg => "blue", :attrs => ["bold"] },
|
|
57
|
+
:snippet => { :fg => "cyan", :bg => "default" },
|
|
58
|
+
:option => { :fg => "white", :bg => "default" },
|
|
59
|
+
:tagged => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
|
|
60
|
+
:draft_notification => { :fg => "red", :bg => "default", :attrs => ["bold"] },
|
|
61
|
+
:completion_character => { :fg => "white", :bg => "default", :attrs => ["bold"] },
|
|
62
|
+
:horizontal_selector_selected => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
|
|
63
|
+
:horizontal_selector_unselected => { :fg => "cyan", :bg => "default" },
|
|
64
|
+
:search_highlight => { :fg => "black", :bg => "yellow", :attrs => ["bold"] },
|
|
65
|
+
:system_buf => { :fg => "blue", :bg => "default" },
|
|
66
|
+
:regular_buf => { :fg => "white", :bg => "default" },
|
|
67
|
+
:modified_buffer => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
|
|
68
|
+
:date => { :fg => "white", :bg => "default"},
|
|
69
|
+
:size_widget => { :fg => "white", :bg => "default"},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def initialize
|
|
73
|
+
raise "only one instance can be created" if @@instance
|
|
74
|
+
@@instance = self
|
|
75
|
+
@color_pairs = {[Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK] => 0}
|
|
76
|
+
@users = []
|
|
77
|
+
@next_id = 0
|
|
78
|
+
reset
|
|
79
|
+
yield self if block_given?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def reset
|
|
83
|
+
@entries = {}
|
|
84
|
+
@highlights = { :none => highlight_sym(:none)}
|
|
85
|
+
@entries[highlight_sym(:none)] = highlight_for(Ncurses::COLOR_WHITE,
|
|
86
|
+
Ncurses::COLOR_BLACK,
|
|
87
|
+
[]) + [nil]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def add sym, fg, bg, attr=nil, highlight=nil
|
|
91
|
+
raise ArgumentError, "color for #{sym} already defined" if @entries.member? sym
|
|
92
|
+
raise ArgumentError, "color '#{fg}' unknown" unless (-1...Ncurses::NUM_COLORS).include? fg
|
|
93
|
+
raise ArgumentError, "color '#{bg}' unknown" unless (-1...Ncurses::NUM_COLORS).include? bg
|
|
94
|
+
attrs = [attr].flatten.compact
|
|
95
|
+
|
|
96
|
+
@entries[sym] = [fg, bg, attrs, nil]
|
|
97
|
+
|
|
98
|
+
if not highlight
|
|
99
|
+
highlight = highlight_sym(sym)
|
|
100
|
+
@entries[highlight] = highlight_for(fg, bg, attrs) + [nil]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
@highlights[sym] = highlight
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def highlight_sym sym
|
|
107
|
+
"#{sym}_highlight".intern
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def highlight_for fg, bg, attrs
|
|
111
|
+
hfg =
|
|
112
|
+
case fg
|
|
113
|
+
when Ncurses::COLOR_BLUE
|
|
114
|
+
Ncurses::COLOR_WHITE
|
|
115
|
+
when Ncurses::COLOR_YELLOW, Ncurses::COLOR_GREEN
|
|
116
|
+
fg
|
|
117
|
+
else
|
|
118
|
+
Ncurses::COLOR_BLACK
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
hbg =
|
|
122
|
+
case bg
|
|
123
|
+
when Ncurses::COLOR_CYAN
|
|
124
|
+
Ncurses::COLOR_YELLOW
|
|
125
|
+
when Ncurses::COLOR_YELLOW
|
|
126
|
+
Ncurses::COLOR_BLUE
|
|
127
|
+
else
|
|
128
|
+
Ncurses::COLOR_CYAN
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
attrs =
|
|
132
|
+
if fg == Ncurses::COLOR_WHITE && attrs.include?(Ncurses::A_BOLD)
|
|
133
|
+
[Ncurses::A_BOLD]
|
|
134
|
+
else
|
|
135
|
+
case hfg
|
|
136
|
+
when Ncurses::COLOR_BLACK
|
|
137
|
+
[]
|
|
138
|
+
else
|
|
139
|
+
[Ncurses::A_BOLD]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
[hfg, hbg, attrs]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def color_for sym, highlight=false
|
|
146
|
+
sym = @highlights[sym] if highlight
|
|
147
|
+
return Ncurses::COLOR_BLACK if sym == :none
|
|
148
|
+
raise ArgumentError, "undefined color #{sym}" unless @entries.member? sym
|
|
149
|
+
|
|
150
|
+
## if this color is cached, return it
|
|
151
|
+
fg, bg, attrs, color = @entries[sym]
|
|
152
|
+
return color if color
|
|
153
|
+
|
|
154
|
+
if(cp = @color_pairs[[fg, bg]])
|
|
155
|
+
## nothing
|
|
156
|
+
else ## need to get a new colorpair
|
|
157
|
+
@next_id = (@next_id + 1) % Ncurses::MAX_PAIRS
|
|
158
|
+
@next_id += 1 if @next_id == 0 # 0 is always white on black
|
|
159
|
+
id = @next_id
|
|
160
|
+
debug "colormap: for color #{sym}, using id #{id} -> #{fg}, #{bg}"
|
|
161
|
+
Ncurses.init_pair id, fg, bg or raise ArgumentError,
|
|
162
|
+
"couldn't initialize curses color pair #{fg}, #{bg} (key #{id})"
|
|
163
|
+
|
|
164
|
+
cp = @color_pairs[[fg, bg]] = Ncurses.COLOR_PAIR(id)
|
|
165
|
+
## delete the old mapping, if it exists
|
|
166
|
+
if @users[cp]
|
|
167
|
+
@users[cp].each do |usym|
|
|
168
|
+
warn "dropping color #{usym} (#{id})"
|
|
169
|
+
@entries[usym][3] = nil
|
|
170
|
+
end
|
|
171
|
+
@users[cp] = []
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
## by now we have a color pair
|
|
176
|
+
color = attrs.inject(cp) { |color, attr| color | attr }
|
|
177
|
+
@entries[sym][3] = color # fill the cache
|
|
178
|
+
(@users[cp] ||= []) << sym # record entry as a user of that color pair
|
|
179
|
+
color
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def sym_is_defined sym
|
|
183
|
+
return sym if @entries.member? sym
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
## Try to use the user defined colors, in case of an error fall back
|
|
187
|
+
## to the default ones.
|
|
188
|
+
def populate_colormap
|
|
189
|
+
user_colors = if File.exists? Redwood::COLOR_FN
|
|
190
|
+
debug "loading user colors from #{Redwood::COLOR_FN}"
|
|
191
|
+
Redwood::load_yaml_obj Redwood::COLOR_FN
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
## Set attachment sybmol to sane default for existing colorschemes
|
|
195
|
+
if user_colors and user_colors.has_key? :to_me
|
|
196
|
+
user_colors[:with_attachment] = user_colors[:to_me] unless user_colors.has_key? :with_attachment
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
Colormap::DEFAULT_COLORS.merge(user_colors||{}).each_pair do |k, v|
|
|
200
|
+
fg = begin
|
|
201
|
+
Ncurses.const_get "COLOR_#{v[:fg].to_s.upcase}"
|
|
202
|
+
rescue NameError
|
|
203
|
+
warn "there is no color named \"#{v[:fg]}\""
|
|
204
|
+
Ncurses::COLOR_GREEN
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
bg = begin
|
|
208
|
+
Ncurses.const_get "COLOR_#{v[:bg].to_s.upcase}"
|
|
209
|
+
rescue NameError
|
|
210
|
+
warn "there is no color named \"#{v[:bg]}\""
|
|
211
|
+
Ncurses::COLOR_RED
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
attrs = (v[:attrs]||[]).map do |a|
|
|
215
|
+
begin
|
|
216
|
+
Ncurses.const_get "A_#{a.upcase}"
|
|
217
|
+
rescue NameError
|
|
218
|
+
warn "there is no attribute named \"#{a}\", using fallback."
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
end.compact
|
|
222
|
+
|
|
223
|
+
highlight_symbol = v[:highlight] ? :"#{v[:highlight]}_color" : nil
|
|
224
|
+
|
|
225
|
+
symbol = (k.to_s + "_color").to_sym
|
|
226
|
+
add symbol, fg, bg, attrs, highlight_symbol
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def self.instance; @@instance; end
|
|
231
|
+
def self.method_missing meth, *a
|
|
232
|
+
Colormap.new unless @@instance
|
|
233
|
+
@@instance.send meth, *a
|
|
234
|
+
end
|
|
235
|
+
# Performance shortcut
|
|
236
|
+
def self.color_for *a; @@instance.color_for *a; end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
end
|
data/lib/sup/contact.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Redwood
|
|
4
|
+
|
|
5
|
+
class ContactManager
|
|
6
|
+
include Redwood::Singleton
|
|
7
|
+
|
|
8
|
+
def initialize fn
|
|
9
|
+
@fn = fn
|
|
10
|
+
|
|
11
|
+
## maintain the mapping between people and aliases. for contacts without
|
|
12
|
+
## aliases, there will be no @a2p entry, so @p2a.keys should be treated
|
|
13
|
+
## as the canonical list of contacts.
|
|
14
|
+
|
|
15
|
+
@p2a = {} # person to alias
|
|
16
|
+
@a2p = {} # alias to person
|
|
17
|
+
@e2p = {} # email to person
|
|
18
|
+
|
|
19
|
+
if File.exists? fn
|
|
20
|
+
IO.foreach(fn) do |l|
|
|
21
|
+
l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
|
|
22
|
+
aalias, addr = $1, $2
|
|
23
|
+
update_alias Person.from_address(addr), aalias
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def contacts; @p2a.keys end
|
|
29
|
+
def contacts_with_aliases; @a2p.values.uniq end
|
|
30
|
+
|
|
31
|
+
def update_alias person, aalias=nil
|
|
32
|
+
old_aalias = @p2a[person]
|
|
33
|
+
if(old_aalias != nil and old_aalias != "") # remove old alias
|
|
34
|
+
@a2p.delete old_aalias
|
|
35
|
+
@e2p.delete person.email
|
|
36
|
+
end
|
|
37
|
+
@p2a[person] = aalias
|
|
38
|
+
unless aalias.nil? || aalias.empty?
|
|
39
|
+
@a2p[aalias] = person
|
|
40
|
+
@e2p[person.email] = person
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
## this may not actually be called anywhere, since we still keep contacts
|
|
45
|
+
## around without aliases to override any fullname changes.
|
|
46
|
+
def drop_contact person
|
|
47
|
+
aalias = @p2a[person]
|
|
48
|
+
@p2a.delete person
|
|
49
|
+
@e2p.delete person.email
|
|
50
|
+
@a2p.delete aalias if aalias
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def contact_for aalias; @a2p[aalias] end
|
|
54
|
+
def alias_for person; @p2a[person] end
|
|
55
|
+
def person_for email; @e2p[email] end
|
|
56
|
+
def is_aliased_contact? person; !@p2a[person].nil? end
|
|
57
|
+
|
|
58
|
+
def save
|
|
59
|
+
File.open(@fn, "w:UTF-8") do |f|
|
|
60
|
+
@p2a.sort_by { |(p, a)| [p.full_address, a] }.each do |(p, a)|
|
|
61
|
+
f.puts "#{a || ''}: #{p.full_address}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|
data/lib/sup/crypto.rb
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'gpgme'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module Redwood
|
|
7
|
+
|
|
8
|
+
class CryptoManager
|
|
9
|
+
include Redwood::Singleton
|
|
10
|
+
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
|
|
14
|
+
[:sign, "Sign"],
|
|
15
|
+
[:sign_and_encrypt, "Sign and encrypt"],
|
|
16
|
+
[:encrypt, "Encrypt only"]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
HookManager.register "gpg-options", <<EOS
|
|
20
|
+
Runs before gpg is called, allowing you to modify the options (most
|
|
21
|
+
likely you would want to add something to certain commands, like
|
|
22
|
+
{:always_trust => true} to encrypting a message, but who knows).
|
|
23
|
+
|
|
24
|
+
Variables:
|
|
25
|
+
operation: what operation will be done ("sign", "encrypt", "decrypt" or "verify")
|
|
26
|
+
options: a dictionary of values to be passed to GPGME
|
|
27
|
+
|
|
28
|
+
Return value: a dictionary to be passed to GPGME
|
|
29
|
+
EOS
|
|
30
|
+
|
|
31
|
+
HookManager.register "sig-output", <<EOS
|
|
32
|
+
Runs when the signature output is being generated, allowing you to
|
|
33
|
+
add extra information to your signatures if you want.
|
|
34
|
+
|
|
35
|
+
Variables:
|
|
36
|
+
signature: the signature object (class is GPGME::Signature)
|
|
37
|
+
from_key: the key that generated the signature (class is GPGME::Key)
|
|
38
|
+
|
|
39
|
+
Return value: an array of lines of output
|
|
40
|
+
EOS
|
|
41
|
+
|
|
42
|
+
HookManager.register "gpg-expand-keys", <<EOS
|
|
43
|
+
Runs when the list of encryption recipients is created, allowing you to
|
|
44
|
+
replace a recipient with one or more GPGME recipients. For example, you could
|
|
45
|
+
replace the email address of a mailing list with the key IDs that belong to
|
|
46
|
+
the recipients of that list. This is essentially what GPG groups do, which
|
|
47
|
+
are not supported by GPGME.
|
|
48
|
+
|
|
49
|
+
Variables:
|
|
50
|
+
recipients: an array of recipients of the current email
|
|
51
|
+
|
|
52
|
+
Return value: an array of recipients (email address or GPG key ID) to encrypt
|
|
53
|
+
the email for
|
|
54
|
+
EOS
|
|
55
|
+
|
|
56
|
+
def initialize
|
|
57
|
+
@mutex = Mutex.new
|
|
58
|
+
|
|
59
|
+
@not_working_reason = nil
|
|
60
|
+
|
|
61
|
+
# test if the gpgme gem is available
|
|
62
|
+
@gpgme_present =
|
|
63
|
+
begin
|
|
64
|
+
begin
|
|
65
|
+
begin
|
|
66
|
+
GPGME.check_version({:protocol => GPGME::PROTOCOL_OpenPGP})
|
|
67
|
+
rescue TypeError
|
|
68
|
+
GPGME.check_version(nil)
|
|
69
|
+
end
|
|
70
|
+
true
|
|
71
|
+
rescue GPGME::Error
|
|
72
|
+
false
|
|
73
|
+
rescue ArgumentError
|
|
74
|
+
# gpgme 2.0.0 raises this due to the hash->string conversion
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
rescue NameError
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
unless @gpgme_present
|
|
82
|
+
@not_working_reason = ['gpgme gem not present',
|
|
83
|
+
'Install the gpgme gem in order to use signed and encrypted emails']
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# if gpg2 is available, it will start gpg-agent if required
|
|
88
|
+
if (bin = `which gpg2`.chomp) =~ /\S/
|
|
89
|
+
if GPGME.respond_to?('set_engine_info')
|
|
90
|
+
GPGME.set_engine_info GPGME::PROTOCOL_OpenPGP, bin, nil
|
|
91
|
+
else
|
|
92
|
+
GPGME.gpgme_set_engine_info GPGME::PROTOCOL_OpenPGP, bin, nil
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
# check if the gpg-options hook uses the passphrase_callback
|
|
96
|
+
# if it doesn't then check if gpg agent is present
|
|
97
|
+
gpg_opts = HookManager.run("gpg-options",
|
|
98
|
+
{:operation => "sign", :options => {}}) || {}
|
|
99
|
+
if gpg_opts[:passphrase_callback].nil?
|
|
100
|
+
if ENV['GPG_AGENT_INFO'].nil?
|
|
101
|
+
@not_working_reason = ["Environment variable 'GPG_AGENT_INFO' not set, is gpg-agent running?",
|
|
102
|
+
"If gpg-agent is running, try $ export `cat ~/.gpg-agent-info`"]
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
gpg_agent_socket_file = ENV['GPG_AGENT_INFO'].split(':')[0]
|
|
107
|
+
unless File.exist?(gpg_agent_socket_file)
|
|
108
|
+
@not_working_reason = ["gpg-agent socket file #{gpg_agent_socket_file} does not exist"]
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
s = File.stat(gpg_agent_socket_file)
|
|
113
|
+
unless s.socket?
|
|
114
|
+
@not_working_reason = ["gpg-agent socket file #{gpg_agent_socket_file} is not a socket"]
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def have_crypto?; @not_working_reason.nil? end
|
|
122
|
+
def not_working_reason; @not_working_reason end
|
|
123
|
+
|
|
124
|
+
def sign from, to, payload
|
|
125
|
+
return unknown_status(@not_working_reason) unless @not_working_reason.nil?
|
|
126
|
+
|
|
127
|
+
gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
|
|
128
|
+
gpg_opts.merge!(gen_sign_user_opts(from))
|
|
129
|
+
gpg_opts = HookManager.run("gpg-options",
|
|
130
|
+
{:operation => "sign", :options => gpg_opts}) || gpg_opts
|
|
131
|
+
begin
|
|
132
|
+
if GPGME.respond_to?('detach_sign')
|
|
133
|
+
sig = GPGME.detach_sign(format_payload(payload), gpg_opts)
|
|
134
|
+
else
|
|
135
|
+
crypto = GPGME::Crypto.new
|
|
136
|
+
gpg_opts[:mode] = GPGME::SIG_MODE_DETACH
|
|
137
|
+
sig = crypto.sign(format_payload(payload), gpg_opts).read
|
|
138
|
+
end
|
|
139
|
+
rescue GPGME::Error => exc
|
|
140
|
+
raise Error, gpgme_exc_msg(exc.message)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# if the key (or gpg-agent) is not available GPGME does not complain
|
|
144
|
+
# but just returns a zero length string. Let's catch that
|
|
145
|
+
if sig.length == 0
|
|
146
|
+
raise Error, gpgme_exc_msg("GPG failed to generate signature: check that gpg-agent is running and your key is available.")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
envelope = RMail::Message.new
|
|
150
|
+
envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature'
|
|
151
|
+
|
|
152
|
+
envelope.add_part payload
|
|
153
|
+
signature = RMail::Message.make_attachment sig, "application/pgp-signature", nil, "signature.asc"
|
|
154
|
+
envelope.add_part signature
|
|
155
|
+
envelope
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def encrypt from, to, payload, sign=false
|
|
159
|
+
return unknown_status(@not_working_reason) unless @not_working_reason.nil?
|
|
160
|
+
|
|
161
|
+
gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
|
|
162
|
+
if sign
|
|
163
|
+
gpg_opts.merge!(gen_sign_user_opts(from))
|
|
164
|
+
gpg_opts.merge!({:sign => true})
|
|
165
|
+
end
|
|
166
|
+
gpg_opts = HookManager.run("gpg-options",
|
|
167
|
+
{:operation => "encrypt", :options => gpg_opts}) || gpg_opts
|
|
168
|
+
recipients = to + [from]
|
|
169
|
+
recipients = HookManager.run("gpg-expand-keys", { :recipients => recipients }) || recipients
|
|
170
|
+
begin
|
|
171
|
+
if GPGME.respond_to?('encrypt')
|
|
172
|
+
cipher = GPGME.encrypt(recipients, format_payload(payload), gpg_opts)
|
|
173
|
+
else
|
|
174
|
+
crypto = GPGME::Crypto.new
|
|
175
|
+
gpg_opts[:recipients] = recipients
|
|
176
|
+
cipher = crypto.encrypt(format_payload(payload), gpg_opts).read
|
|
177
|
+
end
|
|
178
|
+
rescue GPGME::Error => exc
|
|
179
|
+
raise Error, gpgme_exc_msg(exc.message)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# if the key (or gpg-agent) is not available GPGME does not complain
|
|
183
|
+
# but just returns a zero length string. Let's catch that
|
|
184
|
+
if cipher.length == 0
|
|
185
|
+
raise Error, gpgme_exc_msg("GPG failed to generate cipher text: check that gpg-agent is running and your key is available.")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
encrypted_payload = RMail::Message.new
|
|
189
|
+
encrypted_payload.header["Content-Type"] = "application/octet-stream"
|
|
190
|
+
encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"'
|
|
191
|
+
encrypted_payload.body = cipher
|
|
192
|
+
|
|
193
|
+
control = RMail::Message.new
|
|
194
|
+
control.header["Content-Type"] = "application/pgp-encrypted"
|
|
195
|
+
control.header["Content-Disposition"] = "attachment"
|
|
196
|
+
control.body = "Version: 1\n"
|
|
197
|
+
|
|
198
|
+
envelope = RMail::Message.new
|
|
199
|
+
envelope.header["Content-Type"] = 'multipart/encrypted; protocol=application/pgp-encrypted'
|
|
200
|
+
|
|
201
|
+
envelope.add_part control
|
|
202
|
+
envelope.add_part encrypted_payload
|
|
203
|
+
envelope
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def sign_and_encrypt from, to, payload
|
|
207
|
+
encrypt from, to, payload, true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def verified_ok? verify_result
|
|
211
|
+
valid = true
|
|
212
|
+
unknown = false
|
|
213
|
+
all_output_lines = []
|
|
214
|
+
all_trusted = true
|
|
215
|
+
|
|
216
|
+
verify_result.signatures.each do |signature|
|
|
217
|
+
output_lines, trusted = sig_output_lines signature
|
|
218
|
+
all_output_lines << output_lines
|
|
219
|
+
all_output_lines.flatten!
|
|
220
|
+
all_trusted &&= trusted
|
|
221
|
+
|
|
222
|
+
err_code = GPGME::gpgme_err_code(signature.status)
|
|
223
|
+
if err_code == GPGME::GPG_ERR_BAD_SIGNATURE
|
|
224
|
+
valid = false
|
|
225
|
+
elsif err_code != GPGME::GPG_ERR_NO_ERROR
|
|
226
|
+
valid = false
|
|
227
|
+
unknown = true
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if valid || !unknown
|
|
232
|
+
summary_line = simplify_sig_line(verify_result.signatures[0].to_s, all_trusted)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
if all_output_lines.length == 0
|
|
236
|
+
Chunk::CryptoNotice.new :valid, "Encrypted message wasn't signed", all_output_lines
|
|
237
|
+
elsif valid
|
|
238
|
+
if all_trusted
|
|
239
|
+
Chunk::CryptoNotice.new(:valid, summary_line, all_output_lines)
|
|
240
|
+
else
|
|
241
|
+
Chunk::CryptoNotice.new(:valid_untrusted, summary_line, all_output_lines)
|
|
242
|
+
end
|
|
243
|
+
elsif !unknown
|
|
244
|
+
Chunk::CryptoNotice.new(:invalid, summary_line, all_output_lines)
|
|
245
|
+
else
|
|
246
|
+
unknown_status all_output_lines
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def verify payload, signature, detached=true # both RubyMail::Message objects
|
|
251
|
+
return unknown_status(@not_working_reason) unless @not_working_reason.nil?
|
|
252
|
+
|
|
253
|
+
gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP}
|
|
254
|
+
gpg_opts = HookManager.run("gpg-options",
|
|
255
|
+
{:operation => "verify", :options => gpg_opts}) || gpg_opts
|
|
256
|
+
ctx = GPGME::Ctx.new(gpg_opts)
|
|
257
|
+
sig_data = GPGME::Data.from_str signature.decode
|
|
258
|
+
if detached
|
|
259
|
+
signed_text_data = GPGME::Data.from_str(format_payload(payload))
|
|
260
|
+
plain_data = nil
|
|
261
|
+
else
|
|
262
|
+
signed_text_data = nil
|
|
263
|
+
if GPGME::Data.respond_to?('empty')
|
|
264
|
+
plain_data = GPGME::Data.empty
|
|
265
|
+
else
|
|
266
|
+
plain_data = GPGME::Data.empty!
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
begin
|
|
270
|
+
ctx.verify(sig_data, signed_text_data, plain_data)
|
|
271
|
+
rescue GPGME::Error => exc
|
|
272
|
+
return unknown_status [gpgme_exc_msg(exc.message)]
|
|
273
|
+
end
|
|
274
|
+
begin
|
|
275
|
+
self.verified_ok? ctx.verify_result
|
|
276
|
+
rescue ArgumentError => exc
|
|
277
|
+
return unknown_status [gpgme_exc_msg(exc.message)]
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
## returns decrypted_message, status, desc, lines
|
|
282
|
+
def decrypt payload, armor=false # a RubyMail::Message object
|
|
283
|
+
return unknown_status(@not_working_reason) unless @not_working_reason.nil?
|
|
284
|
+
|
|
285
|
+
gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP}
|
|
286
|
+
gpg_opts = HookManager.run("gpg-options",
|
|
287
|
+
{:operation => "decrypt", :options => gpg_opts}) || gpg_opts
|
|
288
|
+
ctx = GPGME::Ctx.new(gpg_opts)
|
|
289
|
+
cipher_data = GPGME::Data.from_str(format_payload(payload))
|
|
290
|
+
if GPGME::Data.respond_to?('empty')
|
|
291
|
+
plain_data = GPGME::Data.empty
|
|
292
|
+
else
|
|
293
|
+
plain_data = GPGME::Data.empty!
|
|
294
|
+
end
|
|
295
|
+
begin
|
|
296
|
+
ctx.decrypt_verify(cipher_data, plain_data)
|
|
297
|
+
rescue GPGME::Error => exc
|
|
298
|
+
return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", gpgme_exc_msg(exc.message))
|
|
299
|
+
end
|
|
300
|
+
begin
|
|
301
|
+
sig = self.verified_ok? ctx.verify_result
|
|
302
|
+
rescue ArgumentError => exc
|
|
303
|
+
sig = unknown_status [gpgme_exc_msg(exc.message)]
|
|
304
|
+
end
|
|
305
|
+
plain_data.seek(0, IO::SEEK_SET)
|
|
306
|
+
output = plain_data.read
|
|
307
|
+
output.transcode(Encoding::ASCII_8BIT, output.encoding)
|
|
308
|
+
|
|
309
|
+
## TODO: test to see if it is still necessary to do a 2nd run if verify
|
|
310
|
+
## fails.
|
|
311
|
+
#
|
|
312
|
+
## check for a valid signature in an extra run because gpg aborts if the
|
|
313
|
+
## signature cannot be verified (but it is still able to decrypt)
|
|
314
|
+
#sigoutput = run_gpg "#{payload_fn.path}"
|
|
315
|
+
#sig = self.old_verified_ok? sigoutput, $?
|
|
316
|
+
|
|
317
|
+
if armor
|
|
318
|
+
msg = RMail::Message.new
|
|
319
|
+
# Look for Charset, they are put before the base64 crypted part
|
|
320
|
+
charsets = payload.body.split("\n").grep(/^Charset:/)
|
|
321
|
+
if !charsets.empty? and charsets[0] =~ /^Charset: (.+)$/
|
|
322
|
+
output.transcode($encoding, $1)
|
|
323
|
+
end
|
|
324
|
+
msg.body = output
|
|
325
|
+
else
|
|
326
|
+
# It appears that some clients use Windows new lines - CRLF - but RMail
|
|
327
|
+
# splits the body and header on "\n\n". So to allow the parse below to
|
|
328
|
+
# succeed, we will convert the newlines to what RMail expects
|
|
329
|
+
output = output.gsub(/\r\n/, "\n")
|
|
330
|
+
# This is gross. This decrypted payload could very well be a multipart
|
|
331
|
+
# element itself, as opposed to a simple payload. For example, a
|
|
332
|
+
# multipart/signed element, like those generated by Mutt when encrypting
|
|
333
|
+
# and signing a message (instead of just clearsigning the body).
|
|
334
|
+
# Supposedly, decrypted_payload being a multipart element ought to work
|
|
335
|
+
# out nicely because Message::multipart_encrypted_to_chunks() runs the
|
|
336
|
+
# decrypted message through message_to_chunks() again to get any
|
|
337
|
+
# children. However, it does not work as intended because these inner
|
|
338
|
+
# payloads need not carry a MIME-Version header, yet they are fed to
|
|
339
|
+
# RMail as a top-level message, for which the MIME-Version header is
|
|
340
|
+
# required. This causes for the part not to be detected as multipart,
|
|
341
|
+
# hence being shown as an attachment. If we detect this is happening,
|
|
342
|
+
# we force the decrypted payload to be interpreted as MIME.
|
|
343
|
+
msg = RMail::Parser.read output
|
|
344
|
+
if msg.header.content_type =~ %r{^multipart/} && !msg.multipart?
|
|
345
|
+
output = "MIME-Version: 1.0\n" + output
|
|
346
|
+
output.fix_encoding!
|
|
347
|
+
msg = RMail::Parser.read output
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
|
|
351
|
+
[notice, sig, msg]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
def unknown_status lines=[]
|
|
357
|
+
Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def gpgme_exc_msg msg
|
|
361
|
+
err_msg = "Exception in GPGME call: #{msg}"
|
|
362
|
+
#info err_msg
|
|
363
|
+
err_msg
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
## here's where we munge rmail output into the format that signed/encrypted
|
|
367
|
+
## PGP/GPG messages should be
|
|
368
|
+
def format_payload payload
|
|
369
|
+
payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n")
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# remove the hex key_id and info in ()
|
|
373
|
+
def simplify_sig_line sig_line, trusted
|
|
374
|
+
sig_line.sub!(/from [0-9A-F]{16} /, "from ")
|
|
375
|
+
if !trusted
|
|
376
|
+
sig_line.sub!(/Good signature/, "Good (untrusted) signature")
|
|
377
|
+
end
|
|
378
|
+
sig_line
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def sig_output_lines signature
|
|
382
|
+
# It appears that the signature.to_s call can lead to a EOFError if
|
|
383
|
+
# the key is not found. So start by looking for the key.
|
|
384
|
+
ctx = GPGME::Ctx.new
|
|
385
|
+
begin
|
|
386
|
+
from_key = ctx.get_key(signature.fingerprint)
|
|
387
|
+
if GPGME::gpgme_err_code(signature.status) == GPGME::GPG_ERR_GENERAL
|
|
388
|
+
first_sig = "General error on signature verification for #{signature.fingerprint}"
|
|
389
|
+
elsif signature.to_s
|
|
390
|
+
first_sig = signature.to_s.sub(/from [0-9A-F]{16} /, 'from "') + '"'
|
|
391
|
+
else
|
|
392
|
+
first_sig = "Unknown error or empty signature"
|
|
393
|
+
end
|
|
394
|
+
rescue EOFError
|
|
395
|
+
from_key = nil
|
|
396
|
+
first_sig = "No public key available for #{signature.fingerprint}"
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
time_line = "Signature made " + signature.timestamp.strftime("%a %d %b %Y %H:%M:%S %Z") +
|
|
400
|
+
" using " + key_type(from_key, signature.fingerprint) +
|
|
401
|
+
"key ID " + signature.fingerprint[-8..-1]
|
|
402
|
+
output_lines = [time_line, first_sig]
|
|
403
|
+
|
|
404
|
+
trusted = false
|
|
405
|
+
if from_key
|
|
406
|
+
# first list all the uids
|
|
407
|
+
if from_key.uids.length > 1
|
|
408
|
+
aka_list = from_key.uids[1..-1]
|
|
409
|
+
aka_list.each { |aka| output_lines << ' aka "' + aka.uid + '"' }
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# now we want to look at the trust of that key
|
|
413
|
+
if signature.validity != GPGME::GPGME_VALIDITY_FULL && signature.validity != GPGME::GPGME_VALIDITY_MARGINAL
|
|
414
|
+
output_lines << "WARNING: This key is not certified with a trusted signature!"
|
|
415
|
+
output_lines << "There is no indication that the signature belongs to the owner"
|
|
416
|
+
output_lines << "Full fingerprint is: " + (0..9).map {|i| signature.fpr[(i*4),4]}.join(":")
|
|
417
|
+
else
|
|
418
|
+
trusted = true
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# finally, run the hook
|
|
422
|
+
output_lines << HookManager.run("sig-output",
|
|
423
|
+
{:signature => signature, :from_key => from_key})
|
|
424
|
+
end
|
|
425
|
+
return output_lines, trusted
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def key_type key, fpr
|
|
429
|
+
return "" if key.nil?
|
|
430
|
+
subkey = key.subkeys.find {|subkey| subkey.fpr == fpr || subkey.keyid == fpr }
|
|
431
|
+
return "" if subkey.nil?
|
|
432
|
+
|
|
433
|
+
case subkey.pubkey_algo
|
|
434
|
+
when GPGME::PK_RSA then "RSA "
|
|
435
|
+
when GPGME::PK_DSA then "DSA "
|
|
436
|
+
when GPGME::PK_ELG then "ElGamel "
|
|
437
|
+
when GPGME::PK_ELG_E then "ElGamel "
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# logic is:
|
|
442
|
+
# if gpgkey set for this account, then use that
|
|
443
|
+
# elsif only one account, then leave blank so gpg default will be user
|
|
444
|
+
# else set --local-user from_email_address
|
|
445
|
+
# NOTE: multiple signers doesn't seem to work with gpgme (2.0.2, 1.0.8)
|
|
446
|
+
#
|
|
447
|
+
def gen_sign_user_opts from
|
|
448
|
+
account = AccountManager.account_for from
|
|
449
|
+
account ||= AccountManager.default_account
|
|
450
|
+
if !account.gpgkey.nil?
|
|
451
|
+
opts = {:signer => account.gpgkey}
|
|
452
|
+
elsif AccountManager.user_emails.length == 1
|
|
453
|
+
# only one account
|
|
454
|
+
opts = {}
|
|
455
|
+
else
|
|
456
|
+
opts = {:signer => from}
|
|
457
|
+
end
|
|
458
|
+
opts
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|