Froze rails gems
[depot.git] / vendor / rails / actionmailer / lib / action_mailer / vendor / tmail-1.2.3 / tmail / encode.rb
1 #--
2 # = COPYRIGHT:
3 #
4 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
5 #
6 # Permission is hereby granted, free of charge, to any person obtaining
7 # a copy of this software and associated documentation files (the
8 # "Software"), to deal in the Software without restriction, including
9 # without limitation the rights to use, copy, modify, merge, publish,
10 # distribute, sublicense, and/or sell copies of the Software, and to
11 # permit persons to whom the Software is furnished to do so, subject to
12 # the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be
15 # included in all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 #
25 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
26 # with permission of Minero Aoki.
27 #++
28 #:stopdoc:
29 require 'nkf'
30 require 'tmail/base64'
31 require 'tmail/stringio'
32 require 'tmail/utils'
33 #:startdoc:
34
35
36 module TMail
37
38 #:stopdoc:
39 class << self
40 attr_accessor :KCODE
41 end
42 self.KCODE = 'NONE'
43
44 module StrategyInterface
45
46 def create_dest( obj )
47 case obj
48 when nil
49 StringOutput.new
50 when String
51 StringOutput.new(obj)
52 when IO, StringOutput
53 obj
54 else
55 raise TypeError, 'cannot handle this type of object for dest'
56 end
57 end
58 module_function :create_dest
59
60 #:startdoc:
61 # Returns the TMail object encoded and ready to be sent via SMTP etc.
62 # You should call this before you are packaging up your email to
63 # correctly escape all the values that need escaping in the email, line
64 # wrap the email etc.
65 #
66 # It is also a good idea to call this before you marshal or serialize
67 # a TMail object.
68 #
69 # For Example:
70 #
71 # email = TMail::Load(my_email_file)
72 # email_to_send = email.encoded
73 def encoded( eol = "\r\n", charset = 'j', dest = nil )
74 accept_strategy Encoder, eol, charset, dest
75 end
76
77 # Returns the TMail object decoded and ready to be used by you, your
78 # program etc.
79 #
80 # You should call this before you are packaging up your email to
81 # correctly escape all the values that need escaping in the email, line
82 # wrap the email etc.
83 #
84 # For Example:
85 #
86 # email = TMail::Load(my_email_file)
87 # email_to_send = email.encoded
88 def decoded( eol = "\n", charset = 'e', dest = nil )
89 # Turn the E-Mail into a string and return it with all
90 # encoded characters decoded. alias for to_s
91 accept_strategy Decoder, eol, charset, dest
92 end
93
94 alias to_s decoded
95
96 def accept_strategy( klass, eol, charset, dest = nil ) #:nodoc:
97 dest ||= ''
98 accept klass.new( create_dest(dest), charset, eol )
99 dest
100 end
101
102 end
103
104 #:stopdoc:
105
106 ###
107 ### MIME B encoding decoder
108 ###
109
110 class Decoder
111
112 include TextUtils
113
114 encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?='
115 ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i
116
117 OUTPUT_ENCODING = {
118 'EUC' => 'e',
119 'SJIS' => 's',
120 }
121
122 def self.decode( str, encoding = nil )
123 encoding ||= (OUTPUT_ENCODING[TMail.KCODE] || 'j')
124 opt = '-mS' + encoding
125 str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
126 end
127
128 def initialize( dest, encoding = nil, eol = "\n" )
129 @f = StrategyInterface.create_dest(dest)
130 @encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
131 @eol = eol
132 end
133
134 def decode( str )
135 self.class.decode(str, @encoding)
136 end
137 private :decode
138
139 def terminate
140 end
141
142 def header_line( str )
143 @f << decode(str)
144 end
145
146 def header_name( nm )
147 @f << nm << ': '
148 end
149
150 def header_body( str )
151 @f << decode(str)
152 end
153
154 def space
155 @f << ' '
156 end
157
158 alias spc space
159
160 def lwsp( str )
161 @f << str
162 end
163
164 def meta( str )
165 @f << str
166 end
167
168 def text( str )
169 @f << decode(str)
170 end
171
172 def phrase( str )
173 @f << quote_phrase(decode(str))
174 end
175
176 def kv_pair( k, v )
177 v = dquote(v) unless token_safe?(v)
178 @f << k << '=' << v
179 end
180
181 def puts( str = nil )
182 @f << str if str
183 @f << @eol
184 end
185
186 def write( str )
187 @f << str
188 end
189
190 end
191
192
193 ###
194 ### MIME B-encoding encoder
195 ###
196
197 #
198 # FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
199 #
200 class Encoder
201
202 include TextUtils
203
204 BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
205
206 def Encoder.encode( str )
207 e = new()
208 e.header_body str
209 e.terminate
210 e.dest.string
211 end
212
213 SPACER = "\t"
214 MAX_LINE_LEN = 78
215 RFC_2822_MAX_LENGTH = 998
216
217 OPTIONS = {
218 'EUC' => '-Ej -m0',
219 'SJIS' => '-Sj -m0',
220 'UTF8' => nil, # FIXME
221 'NONE' => nil
222 }
223
224 def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil )
225 @f = StrategyInterface.create_dest(dest)
226 @opt = OPTIONS[TMail.KCODE]
227 @eol = eol
228 @folded = false
229 @preserve_quotes = true
230 reset
231 end
232
233 def preserve_quotes=( bool )
234 @preserve_quotes
235 end
236
237 def preserve_quotes
238 @preserve_quotes
239 end
240
241 def normalize_encoding( str )
242 if @opt
243 then NKF.nkf(@opt, str)
244 else str
245 end
246 end
247
248 def reset
249 @text = ''
250 @lwsp = ''
251 @curlen = 0
252 end
253
254 def terminate
255 add_lwsp ''
256 reset
257 end
258
259 def dest
260 @f
261 end
262
263 def puts( str = nil )
264 @f << str if str
265 @f << @eol
266 end
267
268 def write( str )
269 @f << str
270 end
271
272 #
273 # add
274 #
275
276 def header_line( line )
277 scanadd line
278 end
279
280 def header_name( name )
281 add_text name.split(/-/).map {|i| i.capitalize }.join('-')
282 add_text ':'
283 add_lwsp ' '
284 end
285
286 def header_body( str )
287 scanadd normalize_encoding(str)
288 end
289
290 def space
291 add_lwsp ' '
292 end
293
294 alias spc space
295
296 def lwsp( str )
297 add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
298 end
299
300 def meta( str )
301 add_text str
302 end
303
304 def text( str )
305 scanadd normalize_encoding(str)
306 end
307
308 def phrase( str )
309 str = normalize_encoding(str)
310 if CONTROL_CHAR === str
311 scanadd str
312 else
313 add_text quote_phrase(str)
314 end
315 end
316
317 # FIXME: implement line folding
318 #
319 def kv_pair( k, v )
320 return if v.nil?
321 v = normalize_encoding(v)
322 if token_safe?(v)
323 add_text k + '=' + v
324 elsif not CONTROL_CHAR === v
325 add_text k + '=' + quote_token(v)
326 else
327 # apply RFC2231 encoding
328 kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
329 add_text kv
330 end
331 end
332
333 def encode_value( str )
334 str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] }
335 end
336
337 private
338
339 def scanadd( str, force = false )
340 types = ''
341 strs = []
342 if str.respond_to?(:encoding)
343 enc = str.encoding
344 str.force_encoding(Encoding::ASCII_8BIT)
345 end
346 until str.empty?
347 if m = /\A[^\e\t\r\n ]+/.match(str)
348 types << (force ? 'j' : 'a')
349 if str.respond_to?(:encoding)
350 strs.push m[0].force_encoding(enc)
351 else
352 strs.push m[0]
353 end
354 elsif m = /\A[\t\r\n ]+/.match(str)
355 types << 's'
356 if str.respond_to?(:encoding)
357 strs.push m[0].force_encoding(enc)
358 else
359 strs.push m[0]
360 end
361
362 elsif m = /\A\e../.match(str)
363 esc = m[0]
364 str = m.post_match
365 if esc != "\e(B" and m = /\A[^\e]+/.match(str)
366 types << 'j'
367 if str.respond_to?(:encoding)
368 strs.push m[0].force_encoding(enc)
369 else
370 strs.push m[0]
371 end
372 end
373
374 else
375 raise 'TMail FATAL: encoder scan fail'
376 end
377 (str = m.post_match) unless m.nil?
378 end
379
380 do_encode types, strs
381 end
382
383 def do_encode( types, strs )
384 #
385 # result : (A|E)(S(A|E))*
386 # E : W(SW)*
387 # W : (J|A)+ but must contain J # (J|A)*J(J|A)*
388 # A : <<A character string not to be encoded>>
389 # J : <<A character string to be encoded>>
390 # S : <<LWSP>>
391 #
392 # An encoding unit is `E'.
393 # Input (parameter `types') is (J|A)(J|A|S)*(J|A)
394 #
395 if BENCODE_DEBUG
396 puts
397 puts '-- do_encode ------------'
398 puts types.split(//).join(' ')
399 p strs
400 end
401
402 e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
403
404 while m = e.match(types)
405 pre = m.pre_match
406 concat_A_S pre, strs[0, pre.size] unless pre.empty?
407 concat_E m[0], strs[m.begin(0) ... m.end(0)]
408 types = m.post_match
409 strs.slice! 0, m.end(0)
410 end
411 concat_A_S types, strs
412 end
413
414 def concat_A_S( types, strs )
415 if RUBY_VERSION < '1.9'
416 a = ?a; s = ?s
417 else
418 a = 'a'.ord; s = 's'.ord
419 end
420 i = 0
421 types.each_byte do |t|
422 case t
423 when a then add_text strs[i]
424 when s then add_lwsp strs[i]
425 else
426 raise "TMail FATAL: unknown flag: #{t.chr}"
427 end
428 i += 1
429 end
430 end
431
432 METHOD_ID = {
433 ?j => :extract_J,
434 ?e => :extract_E,
435 ?a => :extract_A,
436 ?s => :extract_S
437 }
438
439 def concat_E( types, strs )
440 if BENCODE_DEBUG
441 puts '---- concat_E'
442 puts "types=#{types.split(//).join(' ')}"
443 puts "strs =#{strs.inspect}"
444 end
445
446 flush() unless @text.empty?
447
448 chunk = ''
449 strs.each_with_index do |s,i|
450 mid = METHOD_ID[types[i]]
451 until s.empty?
452 unless c = __send__(mid, chunk.size, s)
453 add_with_encode chunk unless chunk.empty?
454 flush
455 chunk = ''
456 fold
457 c = __send__(mid, 0, s)
458 raise 'TMail FATAL: extract fail' unless c
459 end
460 chunk << c
461 end
462 end
463 add_with_encode chunk unless chunk.empty?
464 end
465
466 def extract_J( chunksize, str )
467 size = max_bytes(chunksize, str.size) - 6
468 size = (size % 2 == 0) ? (size) : (size - 1)
469 return nil if size <= 0
470 if str.respond_to?(:encoding)
471 enc = str.encoding
472 str.force_encoding(Encoding::ASCII_8BIT)
473 "\e$B#{str.slice!(0, size)}\e(B".force_encoding(enc)
474 else
475 "\e$B#{str.slice!(0, size)}\e(B"
476 end
477 end
478
479 def extract_A( chunksize, str )
480 size = max_bytes(chunksize, str.size)
481 return nil if size <= 0
482 str.slice!(0, size)
483 end
484
485 alias extract_S extract_A
486
487 def max_bytes( chunksize, ssize )
488 (restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
489 end
490
491 #
492 # free length buffer
493 #
494
495 def add_text( str )
496 @text << str
497 # puts '---- text -------------------------------------'
498 # puts "+ #{str.inspect}"
499 # puts "txt >>>#{@text.inspect}<<<"
500 end
501
502 def add_with_encode( str )
503 @text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
504 end
505
506 def add_lwsp( lwsp )
507 # puts '---- lwsp -------------------------------------'
508 # puts "+ #{lwsp.inspect}"
509 fold if restsize() <= 0
510 flush(@folded)
511 @lwsp = lwsp
512 end
513
514 def flush(folded = false)
515 # puts '---- flush ----'
516 # puts "spc >>>#{@lwsp.inspect}<<<"
517 # puts "txt >>>#{@text.inspect}<<<"
518 @f << @lwsp << @text
519 if folded
520 @curlen = 0
521 else
522 @curlen += (@lwsp.size + @text.size)
523 end
524 @text = ''
525 @lwsp = ''
526 end
527
528 def fold
529 # puts '---- fold ----'
530 unless @f.string =~ /^.*?:$/
531 @f << @eol
532 @lwsp = SPACER
533 else
534 fold_header
535 @folded = true
536 end
537 @curlen = 0
538 end
539
540 def fold_header
541 # Called because line is too long - so we need to wrap.
542 # First look for whitespace in the text
543 # if it has text, fold there
544 # check the remaining text, if too long, fold again
545 # if it doesn't, then don't fold unless the line goes beyond 998 chars
546
547 # Check the text to see if there is whitespace, or if not
548 @wrapped_text = []
549 until @text.blank?
550 fold_the_string
551 end
552 @text = @wrapped_text.join("#{@eol}#{SPACER}")
553 end
554
555 def fold_the_string
556 whitespace_location = @text =~ /\s/ || @text.length
557 # Is the location of the whitespace shorter than the RCF_2822_MAX_LENGTH?
558 # if there is no whitespace in the string, then this
559 unless mazsize(whitespace_location) <= 0
560 @text.strip!
561 @wrapped_text << @text.slice!(0...whitespace_location)
562 # If it is not less, we have to wrap it destructively
563 else
564 slice_point = RFC_2822_MAX_LENGTH - @curlen - @lwsp.length
565 @text.strip!
566 @wrapped_text << @text.slice!(0...slice_point)
567 end
568 end
569
570 def restsize
571 MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
572 end
573
574 def mazsize(whitespace_location)
575 # Per RFC2822, the maximum length of a line is 998 chars
576 RFC_2822_MAX_LENGTH - (@curlen + @lwsp.size + whitespace_location)
577 end
578
579 end
580 #:startdoc:
581 end # module TMail