lonbaker-tmail 1.2.3.1

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 (97) hide show
  1. data/CHANGES +74 -0
  2. data/LICENSE +21 -0
  3. data/NOTES +7 -0
  4. data/README +169 -0
  5. data/ext/Makefile +20 -0
  6. data/ext/tmailscanner/tmail/MANIFEST +4 -0
  7. data/ext/tmailscanner/tmail/depend +1 -0
  8. data/ext/tmailscanner/tmail/extconf.rb +33 -0
  9. data/ext/tmailscanner/tmail/tmailscanner.c +583 -0
  10. data/lib/tmail.rb +5 -0
  11. data/lib/tmail/Makefile +18 -0
  12. data/lib/tmail/address.rb +426 -0
  13. data/lib/tmail/attachments.rb +46 -0
  14. data/lib/tmail/base64.rb +46 -0
  15. data/lib/tmail/compat.rb +41 -0
  16. data/lib/tmail/config.rb +67 -0
  17. data/lib/tmail/core_extensions.rb +63 -0
  18. data/lib/tmail/encode.rb +590 -0
  19. data/lib/tmail/header.rb +962 -0
  20. data/lib/tmail/index.rb +9 -0
  21. data/lib/tmail/interface.rb +1130 -0
  22. data/lib/tmail/loader.rb +3 -0
  23. data/lib/tmail/mail.rb +580 -0
  24. data/lib/tmail/mailbox.rb +496 -0
  25. data/lib/tmail/main.rb +6 -0
  26. data/lib/tmail/mbox.rb +3 -0
  27. data/lib/tmail/net.rb +248 -0
  28. data/lib/tmail/obsolete.rb +132 -0
  29. data/lib/tmail/parser.rb +1476 -0
  30. data/lib/tmail/parser.y +381 -0
  31. data/lib/tmail/port.rb +379 -0
  32. data/lib/tmail/quoting.rb +118 -0
  33. data/lib/tmail/require_arch.rb +58 -0
  34. data/lib/tmail/scanner.rb +49 -0
  35. data/lib/tmail/scanner_r.rb +261 -0
  36. data/lib/tmail/stringio.rb +280 -0
  37. data/lib/tmail/utils.rb +351 -0
  38. data/lib/tmail/version.rb +39 -0
  39. data/meta/MANIFEST +128 -0
  40. data/meta/project.yaml +30 -0
  41. data/meta/unixname +1 -0
  42. data/sample/bench_base64.rb +48 -0
  43. data/sample/data/multipart +23 -0
  44. data/sample/data/normal +29 -0
  45. data/sample/data/sendtest +5 -0
  46. data/sample/data/simple +14 -0
  47. data/sample/data/test +27 -0
  48. data/sample/extract-attachements.rb +33 -0
  49. data/sample/from-check.rb +26 -0
  50. data/sample/multipart.rb +26 -0
  51. data/sample/parse-bench.rb +68 -0
  52. data/sample/parse-test.rb +19 -0
  53. data/sample/sendmail.rb +94 -0
  54. data/test/extctrl.rb +6 -0
  55. data/test/fixtures/mailbox +414 -0
  56. data/test/fixtures/mailbox_without_any_from_or_sender +10 -0
  57. data/test/fixtures/mailbox_without_from +11 -0
  58. data/test/fixtures/mailbox_without_return_path +12 -0
  59. data/test/fixtures/raw_base64_decoded_string +0 -0
  60. data/test/fixtures/raw_base64_email +83 -0
  61. data/test/fixtures/raw_base64_encoded_string +1 -0
  62. data/test/fixtures/raw_email +14 -0
  63. data/test/fixtures/raw_email10 +20 -0
  64. data/test/fixtures/raw_email11 +34 -0
  65. data/test/fixtures/raw_email12 +32 -0
  66. data/test/fixtures/raw_email13 +29 -0
  67. data/test/fixtures/raw_email2 +114 -0
  68. data/test/fixtures/raw_email3 +70 -0
  69. data/test/fixtures/raw_email4 +59 -0
  70. data/test/fixtures/raw_email5 +19 -0
  71. data/test/fixtures/raw_email6 +20 -0
  72. data/test/fixtures/raw_email7 +66 -0
  73. data/test/fixtures/raw_email8 +47 -0
  74. data/test/fixtures/raw_email9 +28 -0
  75. data/test/fixtures/raw_email_quoted_with_0d0a +14 -0
  76. data/test/fixtures/raw_email_reply +32 -0
  77. data/test/fixtures/raw_email_simple +11 -0
  78. data/test/fixtures/raw_email_with_bad_date +48 -0
  79. data/test/fixtures/raw_email_with_illegal_boundary +58 -0
  80. data/test/fixtures/raw_email_with_multipart_mixed_quoted_boundary +50 -0
  81. data/test/fixtures/raw_email_with_nested_attachment +100 -0
  82. data/test/fixtures/raw_email_with_partially_quoted_subject +14 -0
  83. data/test/fixtures/raw_email_with_quoted_illegal_boundary +58 -0
  84. data/test/kcode.rb +14 -0
  85. data/test/test_address.rb +1211 -0
  86. data/test/test_attachments.rb +60 -0
  87. data/test/test_base64.rb +64 -0
  88. data/test/test_encode.rb +85 -0
  89. data/test/test_header.rb +969 -0
  90. data/test/test_helper.rb +9 -0
  91. data/test/test_mail.rb +753 -0
  92. data/test/test_mbox.rb +184 -0
  93. data/test/test_port.rb +436 -0
  94. data/test/test_quote.rb +98 -0
  95. data/test/test_scanner.rb +209 -0
  96. data/test/test_utils.rb +36 -0
  97. metadata +159 -0
@@ -0,0 +1,46 @@
1
+ =begin rdoc
2
+
3
+ = Attachment handling file
4
+
5
+ =end
6
+
7
+ require 'stringio'
8
+
9
+ module TMail
10
+ class Attachment < StringIO
11
+ attr_accessor :original_filename, :content_type, :content_id
12
+ end
13
+
14
+ class Mail
15
+ def has_attachments?
16
+ multipart? && parts.any? { |part| attachment?(part) }
17
+ end
18
+
19
+ def attachment?(part)
20
+ part.disposition_is_attachment? || part.content_type_is_text?
21
+ end
22
+
23
+ def attachments
24
+ if multipart?
25
+ parts.collect { |part|
26
+ if part.multipart?
27
+ part.attachments
28
+ elsif attachment?(part)
29
+ content = part.body # unquoted automatically by TMail#body
30
+ file_name = (part['content-location'] &&
31
+ part['content-location'].body) ||
32
+ part.sub_header("content-type", "name") ||
33
+ part.sub_header("content-disposition", "filename")
34
+
35
+ next if file_name.blank? || content.blank?
36
+
37
+ attachment = Attachment.new(content)
38
+ attachment.original_filename = file_name.strip
39
+ attachment.content_type = part.content_type
40
+ attachment
41
+ end
42
+ }.flatten.compact
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ #--
2
+ # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+ # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24
+ # with permission of Minero Aoki.
25
+ #++
26
+ #:stopdoc:
27
+ module TMail
28
+ module Base64
29
+
30
+ module_function
31
+
32
+ def folding_encode( str, eol = "\n", limit = 60 )
33
+ [str].pack('m')
34
+ end
35
+
36
+ def encode( str )
37
+ [str].pack('m').tr( "\r\n", '' )
38
+ end
39
+
40
+ def decode( str, strict = false )
41
+ str.unpack('m').first
42
+ end
43
+
44
+ end
45
+ end
46
+ #:startdoc:
@@ -0,0 +1,41 @@
1
+ #:stopdoc:
2
+ unless Enumerable.method_defined?(:map)
3
+ module Enumerable #:nodoc:
4
+ alias map collect
5
+ end
6
+ end
7
+
8
+ unless Enumerable.method_defined?(:select)
9
+ module Enumerable #:nodoc:
10
+ alias select find_all
11
+ end
12
+ end
13
+
14
+ unless Enumerable.method_defined?(:reject)
15
+ module Enumerable #:nodoc:
16
+ def reject
17
+ result = []
18
+ each do |i|
19
+ result.push i unless yield(i)
20
+ end
21
+ result
22
+ end
23
+ end
24
+ end
25
+
26
+ unless Enumerable.method_defined?(:sort_by)
27
+ module Enumerable #:nodoc:
28
+ def sort_by
29
+ map {|i| [yield(i), i] }.sort.map {|val, i| i }
30
+ end
31
+ end
32
+ end
33
+
34
+ unless File.respond_to?(:read)
35
+ def File.read(fname) #:nodoc:
36
+ File.open(fname) {|f|
37
+ return f.read
38
+ }
39
+ end
40
+ end
41
+ #:startdoc:
@@ -0,0 +1,67 @@
1
+ #--
2
+ # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+ # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24
+ # with permission of Minero Aoki.
25
+ #++
26
+ #:stopdoc:
27
+ module TMail
28
+
29
+ class Config
30
+
31
+ def initialize( strict )
32
+ @strict_parse = strict
33
+ @strict_base64decode = strict
34
+ end
35
+
36
+ def strict_parse?
37
+ @strict_parse
38
+ end
39
+
40
+ attr_writer :strict_parse
41
+
42
+ def strict_base64decode?
43
+ @strict_base64decode
44
+ end
45
+
46
+ attr_writer :strict_base64decode
47
+
48
+ def new_body_port( mail )
49
+ StringPort.new
50
+ end
51
+
52
+ alias new_preamble_port new_body_port
53
+ alias new_part_port new_body_port
54
+
55
+ end
56
+
57
+ DEFAULT_CONFIG = Config.new(false)
58
+ DEFAULT_STRICT_CONFIG = Config.new(true)
59
+
60
+ def Config.to_config( arg )
61
+ return DEFAULT_STRICT_CONFIG if arg == true
62
+ return DEFAULT_CONFIG if arg == false
63
+ arg or DEFAULT_CONFIG
64
+ end
65
+
66
+ end
67
+ #:startdoc:
@@ -0,0 +1,63 @@
1
+ #:stopdoc:
2
+ unless Object.respond_to?(:blank?)
3
+ class Object
4
+ # Check first to see if we are in a Rails environment, no need to
5
+ # define these methods if we are
6
+
7
+ # An object is blank if it's nil, empty, or a whitespace string.
8
+ # For example, "", " ", nil, [], and {} are blank.
9
+ #
10
+ # This simplifies
11
+ # if !address.nil? && !address.empty?
12
+ # to
13
+ # if !address.blank?
14
+ def blank?
15
+ if respond_to?(:empty?) && respond_to?(:strip)
16
+ empty? or strip.empty?
17
+ elsif respond_to?(:empty?)
18
+ empty?
19
+ else
20
+ !self
21
+ end
22
+ end
23
+ end
24
+
25
+ class NilClass
26
+ def blank?
27
+ true
28
+ end
29
+ end
30
+
31
+ class FalseClass
32
+ def blank?
33
+ true
34
+ end
35
+ end
36
+
37
+ class TrueClass
38
+ def blank?
39
+ false
40
+ end
41
+ end
42
+
43
+ class Array
44
+ alias_method :blank?, :empty?
45
+ end
46
+
47
+ class Hash
48
+ alias_method :blank?, :empty?
49
+ end
50
+
51
+ class String
52
+ def blank?
53
+ empty? || strip.empty?
54
+ end
55
+ end
56
+
57
+ class Numeric
58
+ def blank?
59
+ false
60
+ end
61
+ end
62
+ end
63
+ #:startdoc:
@@ -0,0 +1,590 @@
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
+ SPACER = "\t"
117
+
118
+ OUTPUT_ENCODING = {
119
+ 'EUC' => 'e',
120
+ 'SJIS' => 's',
121
+ }
122
+
123
+ def self.decode( str, encoding = nil )
124
+ encoding ||= (OUTPUT_ENCODING[TMail.KCODE] || 'j')
125
+ opt = '-mS' + encoding
126
+ str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
127
+ end
128
+
129
+ def initialize( dest, encoding = nil, eol = "\n" )
130
+ @f = StrategyInterface.create_dest(dest)
131
+ @encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
132
+ @eol = eol
133
+ end
134
+
135
+ def decode( str )
136
+ self.class.decode(str, @encoding)
137
+ end
138
+ private :decode
139
+
140
+ def terminate
141
+ end
142
+
143
+ def header_line( str )
144
+ @f << decode(str)
145
+ end
146
+
147
+ def header_name( nm )
148
+ @f << nm << ': '
149
+ end
150
+
151
+ def header_body( str )
152
+ @f << decode(str)
153
+ end
154
+
155
+ def space
156
+ @f << ' '
157
+ end
158
+
159
+ alias spc space
160
+
161
+ def lwsp( str )
162
+ @f << str
163
+ end
164
+
165
+ def meta( str )
166
+ @f << str
167
+ end
168
+
169
+ def puts_meta( str )
170
+ @f << str
171
+ end
172
+
173
+ def text( str )
174
+ @f << decode(str)
175
+ end
176
+
177
+ def phrase( str )
178
+ @f << quote_phrase(decode(str))
179
+ end
180
+
181
+ def kv_pair( k, v )
182
+ v = dquote(v) unless token_safe?(v)
183
+ @f << k << '=' << v
184
+ end
185
+
186
+ def puts( str = nil )
187
+ @f << str if str
188
+ @f << @eol
189
+ end
190
+
191
+ def write( str )
192
+ @f << str
193
+ end
194
+
195
+ end
196
+
197
+
198
+ ###
199
+ ### MIME B-encoding encoder
200
+ ###
201
+
202
+ #
203
+ # FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
204
+ #
205
+ class Encoder
206
+
207
+ include TextUtils
208
+
209
+ BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
210
+
211
+ def Encoder.encode( str )
212
+ e = new()
213
+ e.header_body str
214
+ e.terminate
215
+ e.dest.string
216
+ end
217
+
218
+ SPACER = "\t"
219
+ MAX_LINE_LEN = 78
220
+ RFC_2822_MAX_LENGTH = 998
221
+
222
+ OPTIONS = {
223
+ 'EUC' => '-Ej -m0',
224
+ 'SJIS' => '-Sj -m0',
225
+ 'UTF8' => nil, # FIXME
226
+ 'NONE' => nil
227
+ }
228
+
229
+ def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil )
230
+ @f = StrategyInterface.create_dest(dest)
231
+ @opt = OPTIONS[TMail.KCODE]
232
+ @eol = eol
233
+ @folded = false
234
+ @preserve_quotes = true
235
+ reset
236
+ end
237
+
238
+ def preserve_quotes=( bool )
239
+ @preserve_quotes
240
+ end
241
+
242
+ def preserve_quotes
243
+ @preserve_quotes
244
+ end
245
+
246
+ def normalize_encoding( str )
247
+ if @opt
248
+ then NKF.nkf(@opt, str)
249
+ else str
250
+ end
251
+ end
252
+
253
+ def reset
254
+ @text = ''
255
+ @lwsp = ''
256
+ @curlen = 0
257
+ end
258
+
259
+ def terminate
260
+ add_lwsp ''
261
+ reset
262
+ end
263
+
264
+ def dest
265
+ @f
266
+ end
267
+
268
+ def puts( str = nil )
269
+ @f << str if str
270
+ @f << @eol
271
+ end
272
+
273
+ def write( str )
274
+ @f << str
275
+ end
276
+
277
+ #
278
+ # add
279
+ #
280
+
281
+ def header_line( line )
282
+ scanadd line
283
+ end
284
+
285
+ def header_name( name )
286
+ add_text name.split(/-/).map {|i| i.capitalize }.join('-')
287
+ add_text ':'
288
+ add_lwsp ' '
289
+ end
290
+
291
+ def header_body( str )
292
+ scanadd normalize_encoding(str)
293
+ end
294
+
295
+ def space
296
+ add_lwsp ' '
297
+ end
298
+
299
+ alias spc space
300
+
301
+ def lwsp( str )
302
+ add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
303
+ end
304
+
305
+ def meta( str )
306
+ add_text str
307
+ end
308
+
309
+ def puts_meta( str )
310
+ add_text str + @eol + SPACER
311
+ end
312
+
313
+ def text( str )
314
+ scanadd normalize_encoding(str)
315
+ end
316
+
317
+ def phrase( str )
318
+ str = normalize_encoding(str)
319
+ if CONTROL_CHAR === str
320
+ scanadd str
321
+ else
322
+ add_text quote_phrase(str)
323
+ end
324
+ end
325
+
326
+ # FIXME: implement line folding
327
+ #
328
+ def kv_pair( k, v )
329
+ return if v.nil?
330
+ v = normalize_encoding(v)
331
+ if token_safe?(v)
332
+ add_text k + '=' + v
333
+ elsif not CONTROL_CHAR === v
334
+ add_text k + '=' + quote_token(v)
335
+ else
336
+ # apply RFC2231 encoding
337
+ kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
338
+ add_text kv
339
+ end
340
+ end
341
+
342
+ def encode_value( str )
343
+ str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] }
344
+ end
345
+
346
+ private
347
+
348
+ def scanadd( str, force = false )
349
+ types = ''
350
+ strs = []
351
+ if str.respond_to?(:encoding)
352
+ enc = str.encoding
353
+ str.force_encoding(Encoding::ASCII_8BIT)
354
+ end
355
+ until str.empty?
356
+ if m = /\A[^\e\t\r\n ]+/.match(str)
357
+ types << (force ? 'j' : 'a')
358
+ if str.respond_to?(:encoding)
359
+ strs.push m[0].force_encoding(enc)
360
+ else
361
+ strs.push m[0]
362
+ end
363
+ elsif m = /\A[\t\r\n ]+/.match(str)
364
+ types << 's'
365
+ if str.respond_to?(:encoding)
366
+ strs.push m[0].force_encoding(enc)
367
+ else
368
+ strs.push m[0]
369
+ end
370
+
371
+ elsif m = /\A\e../.match(str)
372
+ esc = m[0]
373
+ str = m.post_match
374
+ if esc != "\e(B" and m = /\A[^\e]+/.match(str)
375
+ types << 'j'
376
+ if str.respond_to?(:encoding)
377
+ strs.push m[0].force_encoding(enc)
378
+ else
379
+ strs.push m[0]
380
+ end
381
+ end
382
+
383
+ else
384
+ raise 'TMail FATAL: encoder scan fail'
385
+ end
386
+ (str = m.post_match) unless m.nil?
387
+ end
388
+
389
+ do_encode types, strs
390
+ end
391
+
392
+ def do_encode( types, strs )
393
+ #
394
+ # result : (A|E)(S(A|E))*
395
+ # E : W(SW)*
396
+ # W : (J|A)+ but must contain J # (J|A)*J(J|A)*
397
+ # A : <<A character string not to be encoded>>
398
+ # J : <<A character string to be encoded>>
399
+ # S : <<LWSP>>
400
+ #
401
+ # An encoding unit is `E'.
402
+ # Input (parameter `types') is (J|A)(J|A|S)*(J|A)
403
+ #
404
+ if BENCODE_DEBUG
405
+ puts
406
+ puts '-- do_encode ------------'
407
+ puts types.split(//).join(' ')
408
+ p strs
409
+ end
410
+
411
+ e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
412
+
413
+ while m = e.match(types)
414
+ pre = m.pre_match
415
+ concat_A_S pre, strs[0, pre.size] unless pre.empty?
416
+ concat_E m[0], strs[m.begin(0) ... m.end(0)]
417
+ types = m.post_match
418
+ strs.slice! 0, m.end(0)
419
+ end
420
+ concat_A_S types, strs
421
+ end
422
+
423
+ def concat_A_S( types, strs )
424
+ if RUBY_VERSION < '1.9'
425
+ a = ?a; s = ?s
426
+ else
427
+ a = 'a'.ord; s = 's'.ord
428
+ end
429
+ i = 0
430
+ types.each_byte do |t|
431
+ case t
432
+ when a then add_text strs[i]
433
+ when s then add_lwsp strs[i]
434
+ else
435
+ raise "TMail FATAL: unknown flag: #{t.chr}"
436
+ end
437
+ i += 1
438
+ end
439
+ end
440
+
441
+ METHOD_ID = {
442
+ ?j => :extract_J,
443
+ ?e => :extract_E,
444
+ ?a => :extract_A,
445
+ ?s => :extract_S
446
+ }
447
+
448
+ def concat_E( types, strs )
449
+ if BENCODE_DEBUG
450
+ puts '---- concat_E'
451
+ puts "types=#{types.split(//).join(' ')}"
452
+ puts "strs =#{strs.inspect}"
453
+ end
454
+
455
+ flush() unless @text.empty?
456
+
457
+ chunk = ''
458
+ strs.each_with_index do |s,i|
459
+ mid = METHOD_ID[types[i]]
460
+ until s.empty?
461
+ unless c = __send__(mid, chunk.size, s)
462
+ add_with_encode chunk unless chunk.empty?
463
+ flush
464
+ chunk = ''
465
+ fold
466
+ c = __send__(mid, 0, s)
467
+ raise 'TMail FATAL: extract fail' unless c
468
+ end
469
+ chunk << c
470
+ end
471
+ end
472
+ add_with_encode chunk unless chunk.empty?
473
+ end
474
+
475
+ def extract_J( chunksize, str )
476
+ size = max_bytes(chunksize, str.size) - 6
477
+ size = (size % 2 == 0) ? (size) : (size - 1)
478
+ return nil if size <= 0
479
+ if str.respond_to?(:encoding)
480
+ enc = str.encoding
481
+ str.force_encoding(Encoding::ASCII_8BIT)
482
+ "\e$B#{str.slice!(0, size)}\e(B".force_encoding(enc)
483
+ else
484
+ "\e$B#{str.slice!(0, size)}\e(B"
485
+ end
486
+ end
487
+
488
+ def extract_A( chunksize, str )
489
+ size = max_bytes(chunksize, str.size)
490
+ return nil if size <= 0
491
+ str.slice!(0, size)
492
+ end
493
+
494
+ alias extract_S extract_A
495
+
496
+ def max_bytes( chunksize, ssize )
497
+ (restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
498
+ end
499
+
500
+ #
501
+ # free length buffer
502
+ #
503
+
504
+ def add_text( str )
505
+ @text << str
506
+ # puts '---- text -------------------------------------'
507
+ # puts "+ #{str.inspect}"
508
+ # puts "txt >>>#{@text.inspect}<<<"
509
+ end
510
+
511
+ def add_with_encode( str )
512
+ @text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
513
+ end
514
+
515
+ def add_lwsp( lwsp )
516
+ # puts '---- lwsp -------------------------------------'
517
+ # puts "+ #{lwsp.inspect}"
518
+ fold if restsize() <= 0
519
+ flush(@folded)
520
+ @lwsp = lwsp
521
+ end
522
+
523
+ def flush(folded = false)
524
+ # puts '---- flush ----'
525
+ # puts "spc >>>#{@lwsp.inspect}<<<"
526
+ # puts "txt >>>#{@text.inspect}<<<"
527
+ @f << @lwsp << @text
528
+ if folded
529
+ @curlen = 0
530
+ else
531
+ @curlen += (@lwsp.size + @text.size)
532
+ end
533
+ @text = ''
534
+ @lwsp = ''
535
+ end
536
+
537
+ def fold
538
+ # puts '---- fold ----'
539
+ unless @f.string =~ /^.*?:$/
540
+ @f << @eol
541
+ @lwsp = SPACER
542
+ else
543
+ fold_header
544
+ @folded = true
545
+ end
546
+ @curlen = 0
547
+ end
548
+
549
+ def fold_header
550
+ # Called because line is too long - so we need to wrap.
551
+ # First look for whitespace in the text
552
+ # if it has text, fold there
553
+ # check the remaining text, if too long, fold again
554
+ # if it doesn't, then don't fold unless the line goes beyond 998 chars
555
+
556
+ # Check the text to see if there is whitespace, or if not
557
+ @wrapped_text = []
558
+ until @text.blank?
559
+ fold_the_string
560
+ end
561
+ @text = @wrapped_text.join("#{@eol}#{SPACER}")
562
+ end
563
+
564
+ def fold_the_string
565
+ whitespace_location = @text =~ /\s/ || @text.length
566
+ # Is the location of the whitespace shorter than the RCF_2822_MAX_LENGTH?
567
+ # if there is no whitespace in the string, then this
568
+ unless mazsize(whitespace_location) <= 0
569
+ @text.strip!
570
+ @wrapped_text << @text.slice!(0...whitespace_location)
571
+ # If it is not less, we have to wrap it destructively
572
+ else
573
+ slice_point = RFC_2822_MAX_LENGTH - @curlen - @lwsp.length
574
+ @text.strip!
575
+ @wrapped_text << @text.slice!(0...slice_point)
576
+ end
577
+ end
578
+
579
+ def restsize
580
+ MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
581
+ end
582
+
583
+ def mazsize(whitespace_location)
584
+ # Per RFC2822, the maximum length of a line is 998 chars
585
+ RFC_2822_MAX_LENGTH - (@curlen + @lwsp.size + whitespace_location)
586
+ end
587
+
588
+ end
589
+ #:startdoc:
590
+ end # module TMail