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.
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