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,134 @@
1
+ module Redwood
2
+
3
+ class Person
4
+ attr_accessor :name, :email
5
+
6
+ def initialize name, email
7
+ raise ArgumentError, "email can't be nil" unless email
8
+
9
+ email.fix_encoding!
10
+
11
+ @name = if name
12
+ name.fix_encoding!
13
+ name = name.strip.gsub(/\s+/, " ")
14
+ name =~ /^(['"]\s*)(.*?)(\s*["'])$/ ? $2 : name
15
+ name.gsub('\\\\', '\\')
16
+ end
17
+
18
+ @email = email.strip.gsub(/\s+/, " ")
19
+ end
20
+
21
+ def to_s; "#@name <#@email>" end
22
+
23
+ # def == o; o && o.email == email; end
24
+ # alias :eql? :==
25
+ # def hash; [name, email].hash; end
26
+
27
+ def shortname
28
+ case @name
29
+ when /\S+, (\S+)/
30
+ $1
31
+ when /(\S+) \S+/
32
+ $1
33
+ when nil
34
+ @email
35
+ else
36
+ @name
37
+ end
38
+ end
39
+
40
+ def longname
41
+ if @name && @email
42
+ "#@name <#@email>"
43
+ else
44
+ @email
45
+ end
46
+ end
47
+
48
+ def mediumname; @name || @email; end
49
+
50
+ def Person.full_address name, email
51
+ if name && email
52
+ if name =~ /[",@]/
53
+ "#{name.inspect} <#{email}>" # escape quotes
54
+ else
55
+ "#{name} <#{email}>"
56
+ end
57
+ else
58
+ email
59
+ end
60
+ end
61
+
62
+ def full_address
63
+ Person.full_address @name, @email
64
+ end
65
+
66
+ ## when sorting addresses, sort by this
67
+ def sort_by_me
68
+ case @name
69
+ when /^(\S+), \S+/
70
+ $1
71
+ when /^\S+ \S+ (\S+)/
72
+ $1
73
+ when /^\S+ (\S+)/
74
+ $1
75
+ when nil
76
+ @email
77
+ else
78
+ @name
79
+ end.downcase
80
+ end
81
+
82
+ ## return "canonical" person using contact manager or create one if
83
+ ## not found or contact manager not available
84
+ def self.from_name_and_email name, email
85
+ ContactManager.instantiated? && ContactManager.person_for(email) || Person.new(name, email)
86
+ end
87
+
88
+ def self.from_address s
89
+ return nil if s.nil?
90
+
91
+ ## try and parse an email address and name
92
+ name, email = case s
93
+ when /(.+?) ((\S+?)@\S+) \3/
94
+ ## ok, this first match cause is insane, but bear with me. email
95
+ ## addresses are stored in the to/from/etc fields of the index in a
96
+ ## weird format: "name address first-part-of-address", i.e. spaces
97
+ ## separating those three bits, and no <>'s. this is the output of
98
+ ## #indexable_content. here, we reverse-engineer that format to extract
99
+ ## a valid address.
100
+ ##
101
+ ## we store things this way to allow searches on a to/from/etc field to
102
+ ## match any of those parts. a more robust solution would be to store a
103
+ ## separate, non-indexed field with the proper headers. but this way we
104
+ ## save precious bits, and it's backwards-compatible with older indexes.
105
+ [$1, $2]
106
+ when /["'](.*?)["'] <(.*?)>/, /([^,]+) <(.*?)>/
107
+ a, b = $1, $2
108
+ [a.gsub('\"', '"'), b]
109
+ when /<((\S+?)@\S+?)>/
110
+ [$2, $1]
111
+ when /((\S+?)@\S+)/
112
+ [$2, $1]
113
+ else
114
+ [nil, s]
115
+ end
116
+
117
+ from_name_and_email name, email
118
+ end
119
+
120
+ def self.from_address_list ss
121
+ return [] if ss.nil?
122
+ ss.dup.split_on_commas.map { |s| self.from_address s }
123
+ end
124
+
125
+ ## see comments in self.from_address
126
+ def indexable_content
127
+ [name, email, email.split(/@/).first].join(" ")
128
+ end
129
+
130
+ def eql? o; email.eql? o.email end
131
+ def hash; email.hash end
132
+ end
133
+
134
+ end
@@ -0,0 +1,272 @@
1
+ require 'thread'
2
+
3
+ module Redwood
4
+
5
+ class PollManager
6
+ include Redwood::Singleton
7
+
8
+ HookManager.register "before-add-message", <<EOS
9
+ Executes immediately before a message is added to the index.
10
+ Variables:
11
+ message: the new message
12
+ EOS
13
+
14
+ HookManager.register "before-poll", <<EOS
15
+ Executes immediately before a poll for new messages commences.
16
+ No variables.
17
+ EOS
18
+
19
+ HookManager.register "after-poll", <<EOS
20
+ Executes immediately after a poll for new messages completes.
21
+ Variables:
22
+ num: the total number of new messages added in this poll
23
+ num_inbox: the number of new messages added in this poll which
24
+ appear in the inbox (i.e. were not auto-archived).
25
+ num_total: the total number of messages
26
+ num_inbox_total: the total number of new messages in the inbox.
27
+ num_inbox_total_unread: the total number of unread messages in the inbox
28
+ num_updated: the total number of updated messages
29
+ num_deleted: the total number of deleted messages
30
+ labels: the labels that were applied
31
+ from_and_subj: an array of (from email address, subject) pairs
32
+ from_and_subj_inbox: an array of (from email address, subject) pairs for
33
+ only those messages appearing in the inbox
34
+ EOS
35
+
36
+ def initialize
37
+ @delay = $config[:poll_interval] || 300
38
+ @mutex = Mutex.new
39
+ @thread = nil
40
+ @last_poll = nil
41
+ @polling = Mutex.new
42
+ @poll_sources = nil
43
+ @mode = nil
44
+ @should_clear_running_totals = false
45
+ clear_running_totals # defines @running_totals
46
+ UpdateManager.register self
47
+ end
48
+
49
+ def poll_with_sources
50
+ @mode ||= PollMode.new
51
+
52
+ if HookManager.enabled? "before-poll"
53
+ HookManager.run("before-poll")
54
+ else
55
+ BufferManager.flash "Polling for new messages..."
56
+ end
57
+
58
+ num, numi, numu, numd, from_and_subj, from_and_subj_inbox, loaded_labels = @mode.poll
59
+ clear_running_totals if @should_clear_running_totals
60
+ @running_totals[:num] += num
61
+ @running_totals[:numi] += numi
62
+ @running_totals[:numu] += numu
63
+ @running_totals[:numd] += numd
64
+ @running_totals[:loaded_labels] += loaded_labels || []
65
+
66
+
67
+ if HookManager.enabled? "after-poll"
68
+ hook_args = { :num => num, :num_inbox => numi,
69
+ :num_total => @running_totals[:num], :num_inbox_total => @running_totals[:numi],
70
+ :num_updated => @running_totals[:numu],
71
+ :num_deleted => @running_totals[:numd],
72
+ :labels => @running_totals[:loaded_labels],
73
+ :from_and_subj => from_and_subj, :from_and_subj_inbox => from_and_subj_inbox,
74
+ :num_inbox_total_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] } }
75
+
76
+ HookManager.run("after-poll", hook_args)
77
+ else
78
+ if @running_totals[:num] > 0
79
+ flash_msg = "Loaded #{@running_totals[:num].pluralize 'new message'}, #{@running_totals[:numi]} to inbox. " if @running_totals[:num] > 0
80
+ flash_msg += "Updated #{@running_totals[:numu].pluralize 'message'}. " if @running_totals[:numu] > 0
81
+ flash_msg += "Deleted #{@running_totals[:numd].pluralize 'message'}. " if @running_totals[:numd] > 0
82
+ flash_msg += "Labels: #{@running_totals[:loaded_labels].map{|l| l.to_s}.join(', ')}." if @running_totals[:loaded_labels].size > 0
83
+ BufferManager.flash flash_msg
84
+ else
85
+ BufferManager.flash "No new messages."
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ def poll
92
+ if @polling.try_lock
93
+ @poll_sources = SourceManager.usual_sources
94
+ num, numi = poll_with_sources
95
+ @polling.unlock
96
+ [num, numi]
97
+ else
98
+ debug "poll already in progress."
99
+ return
100
+ end
101
+ end
102
+
103
+ def poll_unusual
104
+ if @polling.try_lock
105
+ @poll_sources = SourceManager.unusual_sources
106
+ num, numi = poll_with_sources
107
+ @polling.unlock
108
+ [num, numi]
109
+ else
110
+ debug "poll_unusual already in progress."
111
+ return
112
+ end
113
+ end
114
+
115
+ def start
116
+ @thread = Redwood::reporting_thread("periodic poll") do
117
+ while true
118
+ sleep @delay / 2
119
+ poll if @last_poll.nil? || (Time.now - @last_poll) >= @delay
120
+ end
121
+ end
122
+ end
123
+
124
+ def stop
125
+ @thread.kill if @thread
126
+ @thread = nil
127
+ end
128
+
129
+ def do_poll
130
+ total_num = total_numi = total_numu = total_numd = 0
131
+ from_and_subj = []
132
+ from_and_subj_inbox = []
133
+ loaded_labels = Set.new
134
+
135
+ @mutex.synchronize do
136
+ @poll_sources.each do |source|
137
+ begin
138
+ yield "Loading from #{source}... "
139
+ rescue SourceError => e
140
+ warn "problem getting messages from #{source}: #{e.message}"
141
+ next
142
+ end
143
+
144
+ msg = ""
145
+ num = numi = numu = numd = 0
146
+ poll_from source do |action,m,old_m,progress|
147
+ if action == :delete
148
+ yield "Deleting #{m.id}"
149
+ loaded_labels.merge m.labels
150
+ numd += 1
151
+ elsif action == :update
152
+ yield "Message at #{m.source_info} is an update of an old message. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
153
+ loaded_labels.merge m.labels
154
+ numu += 1
155
+ elsif action == :add
156
+ if old_m
157
+ new_locations = (m.locations - old_m.locations)
158
+ if not new_locations.empty?
159
+ yield "Message at #{new_locations[0].info} has changed its source location. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
160
+ numu += 1
161
+ else
162
+ yield "Skipping already-imported message at #{m.locations[-1].info}"
163
+ end
164
+ else
165
+ yield "Found new message at #{m.source_info} with labels #{m.labels.to_a * ','}"
166
+ loaded_labels.merge m.labels
167
+ num += 1
168
+ from_and_subj << [m.from && m.from.longname, m.subj]
169
+ if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
170
+ from_and_subj_inbox << [m.from && m.from.longname, m.subj]
171
+ numi += 1
172
+ end
173
+ end
174
+ else fail
175
+ end
176
+ end
177
+ msg += "Found #{num} messages, #{numi} to inbox. " unless num == 0
178
+ msg += "Updated #{numu} messages. " unless numu == 0
179
+ msg += "Deleted #{numd} messages." unless numd == 0
180
+ yield msg unless msg == ""
181
+ total_num += num
182
+ total_numi += numi
183
+ total_numu += numu
184
+ total_numd += numd
185
+ end
186
+
187
+ loaded_labels = loaded_labels - LabelManager::HIDDEN_RESERVED_LABELS - [:inbox, :killed]
188
+ yield "Done polling; loaded #{total_num} new messages total"
189
+ @last_poll = Time.now
190
+ end
191
+ [total_num, total_numi, total_numu, total_numd, from_and_subj, from_and_subj_inbox, loaded_labels]
192
+ end
193
+
194
+ ## like Source#poll, but yields successive Message objects, which have their
195
+ ## labels and locations set correctly. The Messages are saved to or removed
196
+ ## from the index after being yielded.
197
+ def poll_from source, opts={}
198
+ debug "trying to acquire poll lock for: #{source}..."
199
+ if source.try_lock
200
+ begin
201
+ source.poll do |sym, args|
202
+ case sym
203
+ when :add
204
+ m = Message.build_from_source source, args[:info]
205
+ old_m = Index.build_message m.id
206
+ m.labels += args[:labels]
207
+ m.labels.delete :inbox if source.archived?
208
+ m.labels.delete :unread if source.read?
209
+ m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
210
+ m.labels.each { |l| LabelManager << l }
211
+ m.labels = old_m.labels + (m.labels - [:unread, :inbox]) if old_m
212
+ m.locations = old_m.locations + m.locations if old_m
213
+ HookManager.run "before-add-message", :message => m
214
+ yield :add, m, old_m, args[:progress] if block_given?
215
+ Index.sync_message m, true
216
+
217
+ if Index.message_joining_killed? m
218
+ m.labels += [:killed]
219
+ Index.sync_message m, true
220
+ end
221
+
222
+ ## We need to add or unhide the message when it either did not exist
223
+ ## before at all or when it was updated. We do *not* add/unhide when
224
+ ## the same message was found at a different location
225
+ if old_m
226
+ UpdateManager.relay self, :updated, m
227
+ elsif !old_m or not old_m.locations.member? m.location
228
+ UpdateManager.relay self, :added, m
229
+ end
230
+ when :delete
231
+ Index.each_message({:location => [source.id, args[:info]]}, false) do |m|
232
+ m.locations.delete Location.new(source, args[:info])
233
+ Index.sync_message m, false
234
+ if m.locations.size == 0
235
+ yield :delete, m, [source,args[:info]], args[:progress] if block_given?
236
+ Index.delete m.id
237
+ UpdateManager.relay self, :location_deleted, m
238
+ end
239
+ end
240
+ when :update
241
+ Index.each_message({:location => [source.id, args[:old_info]]}, false) do |m|
242
+ old_m = Index.build_message m.id
243
+ m.locations.delete Location.new(source, args[:old_info])
244
+ m.locations.push Location.new(source, args[:new_info])
245
+ ## Update labels that might have been modified remotely
246
+ m.labels -= source.supported_labels?
247
+ m.labels += args[:labels]
248
+ yield :update, m, old_m if block_given?
249
+ Index.sync_message m, true
250
+ UpdateManager.relay self, :updated, m
251
+ end
252
+ end
253
+ end
254
+
255
+ rescue SourceError => e
256
+ warn "problem getting messages from #{source}: #{e.message}"
257
+
258
+ ensure
259
+ source.go_idle
260
+ source.unlock
261
+ end
262
+ else
263
+ debug "source #{source} is already being polled."
264
+ end
265
+ end
266
+
267
+ def handle_idle_update sender, idle_since; @should_clear_running_totals = false; end
268
+ def handle_unidle_update sender, idle_since; @should_clear_running_totals = true; clear_running_totals; end
269
+ def clear_running_totals; @running_totals = {:num => 0, :numi => 0, :numu => 0, :numd => 0, :loaded_labels => Set.new}; end
270
+ end
271
+
272
+ end
@@ -0,0 +1,56 @@
1
+ ## from: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/101949
2
+
3
+ # $Id: rfc2047.rb,v 1.4 2003/04/18 20:55:56 sam Exp $
4
+ # MODIFIED slightly by William Morgan
5
+ #
6
+ # An implementation of RFC 2047 decoding.
7
+ #
8
+ # This module depends on the iconv library by Nobuyoshi Nakada, which I've
9
+ # heard may be distributed as a standard part of Ruby 1.8. Many thanks to him
10
+ # for helping with building and using iconv.
11
+ #
12
+ # Thanks to "Josef 'Jupp' Schugt" <jupp / gmx.de> for pointing out an error with
13
+ # stateful character sets.
14
+ #
15
+ # Copyright (c) Sam Roberts <sroberts / uniserve.com> 2004
16
+ #
17
+ # This file is distributed under the same terms as Ruby.
18
+
19
+ module Rfc2047
20
+ WORD = %r{=\?([!\#$%&'*+-/0-9A-Z\\^\`a-z{|}~]+)\?([BbQq])\?([!->@-~]+)\?=} # :nodoc: 'stupid ruby-mode
21
+ WORDSEQ = %r{(#{WORD.source})\s+(?=#{WORD.source})}
22
+
23
+ def Rfc2047.is_encoded? s; s =~ WORD end
24
+
25
+ # Decodes a string, +from+, containing RFC 2047 encoded words into a target
26
+ # character set, +target+. See iconv_open(3) for information on the
27
+ # supported target encodings. If one of the encoded words cannot be
28
+ # converted to the target encoding, it is left in its encoded form.
29
+ def Rfc2047.decode_to(target, from)
30
+ from = from.gsub(WORDSEQ, '\1')
31
+ out = from.gsub(WORD) do
32
+ |word|
33
+ charset, encoding, text = $1, $2, $3
34
+
35
+ # B64 or QP decode, as necessary:
36
+ case encoding
37
+ when 'b', 'B'
38
+ #puts text
39
+ text = text.unpack('m*')[0]
40
+ #puts text.dump
41
+
42
+ when 'q', 'Q'
43
+ # RFC 2047 has a variant of quoted printable where a ' ' character
44
+ # can be represented as an '_', rather than =32, so convert
45
+ # any of these that we find before doing the QP decoding.
46
+ text = text.tr("_", " ")
47
+ text = text.unpack('M*')[0]
48
+
49
+ # Don't need an else, because no other values can be matched in a
50
+ # WORD.
51
+ end
52
+
53
+ text.transcode(target, charset)
54
+ end
55
+ end
56
+ end