sup 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +12 -0
  4. data/CONTRIBUTORS +84 -0
  5. data/Gemfile +3 -0
  6. data/HACKING +42 -0
  7. data/History.txt +361 -0
  8. data/LICENSE +280 -0
  9. data/README.md +70 -0
  10. data/Rakefile +12 -0
  11. data/ReleaseNotes +231 -0
  12. data/bin/sup +434 -0
  13. data/bin/sup-add +118 -0
  14. data/bin/sup-config +243 -0
  15. data/bin/sup-dump +43 -0
  16. data/bin/sup-import-dump +101 -0
  17. data/bin/sup-psych-ify-config-files +21 -0
  18. data/bin/sup-recover-sources +87 -0
  19. data/bin/sup-sync +210 -0
  20. data/bin/sup-sync-back-maildir +127 -0
  21. data/bin/sup-tweak-labels +140 -0
  22. data/contrib/colorpicker.rb +100 -0
  23. data/contrib/completion/_sup.zsh +114 -0
  24. data/devel/console.sh +3 -0
  25. data/devel/count-loc.sh +3 -0
  26. data/devel/load-index.rb +9 -0
  27. data/devel/profile.rb +12 -0
  28. data/devel/start-console.rb +5 -0
  29. data/doc/FAQ.txt +119 -0
  30. data/doc/Hooks.txt +79 -0
  31. data/doc/Philosophy.txt +69 -0
  32. data/lib/sup.rb +467 -0
  33. data/lib/sup/account.rb +90 -0
  34. data/lib/sup/buffer.rb +768 -0
  35. data/lib/sup/colormap.rb +239 -0
  36. data/lib/sup/contact.rb +67 -0
  37. data/lib/sup/crypto.rb +461 -0
  38. data/lib/sup/draft.rb +119 -0
  39. data/lib/sup/hook.rb +159 -0
  40. data/lib/sup/horizontal_selector.rb +59 -0
  41. data/lib/sup/idle.rb +42 -0
  42. data/lib/sup/index.rb +882 -0
  43. data/lib/sup/interactive_lock.rb +89 -0
  44. data/lib/sup/keymap.rb +140 -0
  45. data/lib/sup/label.rb +87 -0
  46. data/lib/sup/logger.rb +77 -0
  47. data/lib/sup/logger/singleton.rb +10 -0
  48. data/lib/sup/maildir.rb +257 -0
  49. data/lib/sup/mbox.rb +187 -0
  50. data/lib/sup/message.rb +803 -0
  51. data/lib/sup/message_chunks.rb +328 -0
  52. data/lib/sup/mode.rb +140 -0
  53. data/lib/sup/modes/buffer_list_mode.rb +50 -0
  54. data/lib/sup/modes/completion_mode.rb +55 -0
  55. data/lib/sup/modes/compose_mode.rb +38 -0
  56. data/lib/sup/modes/console_mode.rb +125 -0
  57. data/lib/sup/modes/contact_list_mode.rb +148 -0
  58. data/lib/sup/modes/edit_message_async_mode.rb +110 -0
  59. data/lib/sup/modes/edit_message_mode.rb +728 -0
  60. data/lib/sup/modes/file_browser_mode.rb +109 -0
  61. data/lib/sup/modes/forward_mode.rb +82 -0
  62. data/lib/sup/modes/help_mode.rb +19 -0
  63. data/lib/sup/modes/inbox_mode.rb +85 -0
  64. data/lib/sup/modes/label_list_mode.rb +138 -0
  65. data/lib/sup/modes/label_search_results_mode.rb +38 -0
  66. data/lib/sup/modes/line_cursor_mode.rb +203 -0
  67. data/lib/sup/modes/log_mode.rb +57 -0
  68. data/lib/sup/modes/person_search_results_mode.rb +12 -0
  69. data/lib/sup/modes/poll_mode.rb +19 -0
  70. data/lib/sup/modes/reply_mode.rb +228 -0
  71. data/lib/sup/modes/resume_mode.rb +52 -0
  72. data/lib/sup/modes/scroll_mode.rb +252 -0
  73. data/lib/sup/modes/search_list_mode.rb +204 -0
  74. data/lib/sup/modes/search_results_mode.rb +59 -0
  75. data/lib/sup/modes/text_mode.rb +76 -0
  76. data/lib/sup/modes/thread_index_mode.rb +1033 -0
  77. data/lib/sup/modes/thread_view_mode.rb +941 -0
  78. data/lib/sup/person.rb +134 -0
  79. data/lib/sup/poll.rb +272 -0
  80. data/lib/sup/rfc2047.rb +56 -0
  81. data/lib/sup/search.rb +110 -0
  82. data/lib/sup/sent.rb +58 -0
  83. data/lib/sup/service/label_service.rb +45 -0
  84. data/lib/sup/source.rb +244 -0
  85. data/lib/sup/tagger.rb +50 -0
  86. data/lib/sup/textfield.rb +253 -0
  87. data/lib/sup/thread.rb +452 -0
  88. data/lib/sup/time.rb +93 -0
  89. data/lib/sup/undo.rb +38 -0
  90. data/lib/sup/update.rb +30 -0
  91. data/lib/sup/util.rb +747 -0
  92. data/lib/sup/util/ncurses.rb +274 -0
  93. data/lib/sup/util/path.rb +9 -0
  94. data/lib/sup/util/query.rb +17 -0
  95. data/lib/sup/util/uri.rb +15 -0
  96. data/lib/sup/version.rb +3 -0
  97. data/sup.gemspec +53 -0
  98. data/test/dummy_source.rb +61 -0
  99. data/test/gnupg_test_home/gpg.conf +1 -0
  100. data/test/gnupg_test_home/pubring.gpg +0 -0
  101. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  102. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  103. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  104. data/test/gnupg_test_home/secring.gpg +0 -0
  105. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
  106. data/test/gnupg_test_home/trustdb.gpg +0 -0
  107. data/test/integration/test_label_service.rb +18 -0
  108. data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
  109. data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
  110. data/test/messages/missing-line.eml +9 -0
  111. data/test/test_crypto.rb +109 -0
  112. data/test/test_header_parsing.rb +168 -0
  113. data/test/test_helper.rb +7 -0
  114. data/test/test_message.rb +532 -0
  115. data/test/test_messages_dir.rb +147 -0
  116. data/test/test_yaml_migration.rb +85 -0
  117. data/test/test_yaml_regressions.rb +17 -0
  118. data/test/unit/service/test_label_service.rb +19 -0
  119. data/test/unit/test_horizontal_selector.rb +40 -0
  120. data/test/unit/util/test_query.rb +46 -0
  121. data/test/unit/util/test_string.rb +57 -0
  122. data/test/unit/util/test_uri.rb +19 -0
  123. metadata +423 -0
@@ -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
@@ -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
@@ -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