mailparser 0.4.22a
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.
- data/HISTORY +141 -0
- data/README.txt +501 -0
- data/lib/mailparser.rb +558 -0
- data/lib/mailparser/conv_charset.rb +27 -0
- data/lib/mailparser/error.rb +7 -0
- data/lib/mailparser/loose.rb +292 -0
- data/lib/mailparser/obsolete.rb +403 -0
- data/lib/mailparser/rfc2045.rb +54 -0
- data/lib/mailparser/rfc2045/parser.rb +245 -0
- data/lib/mailparser/rfc2045/scanner.rb +54 -0
- data/lib/mailparser/rfc2047.rb +82 -0
- data/lib/mailparser/rfc2183.rb +33 -0
- data/lib/mailparser/rfc2183/parser.rb +186 -0
- data/lib/mailparser/rfc2183/scanner.rb +7 -0
- data/lib/mailparser/rfc2231.rb +57 -0
- data/lib/mailparser/rfc2822.rb +212 -0
- data/lib/mailparser/rfc2822/parser.rb +883 -0
- data/lib/mailparser/rfc2822/scanner.rb +119 -0
- data/test.rb +26 -0
- data/test/test_loose.rb +371 -0
- data/test/test_mailparser.rb +1130 -0
- data/test/test_obsolete.rb +615 -0
- data/test/test_rfc2045.rb +121 -0
- data/test/test_rfc2047.rb +118 -0
- data/test/test_rfc2183.rb +60 -0
- data/test/test_rfc2231.rb +167 -0
- data/test/test_rfc2822.rb +370 -0
- metadata +81 -0
data/lib/mailparser.rb
ADDED
@@ -0,0 +1,558 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# Copyright (C) 2006-2010 TOMITA Masahiro
|
3
|
+
# mailto:tommy@tmtm.org
|
4
|
+
|
5
|
+
require "mailparser/error"
|
6
|
+
require "mailparser/rfc2045"
|
7
|
+
require "mailparser/rfc2047"
|
8
|
+
require "mailparser/rfc2183"
|
9
|
+
require "mailparser/rfc2231"
|
10
|
+
require "mailparser/rfc2822"
|
11
|
+
require "mailparser/loose"
|
12
|
+
require "mailparser/conv_charset"
|
13
|
+
|
14
|
+
require "stringio"
|
15
|
+
require "tempfile"
|
16
|
+
|
17
|
+
# メールをパースする。
|
18
|
+
#
|
19
|
+
# m = MailParser.new
|
20
|
+
# m.parse(src)
|
21
|
+
# m.header => #<MailParser::Header>
|
22
|
+
# m.body => パースされた本文文字列
|
23
|
+
# m.part => [#<Mailparser>, ...]
|
24
|
+
#
|
25
|
+
module MailParser
|
26
|
+
include RFC2045, RFC2183, RFC2822
|
27
|
+
|
28
|
+
HEADER_PARSER = {
|
29
|
+
"date" => RFC2822,
|
30
|
+
"from" => RFC2822,
|
31
|
+
"sender" => RFC2822,
|
32
|
+
"reply-to" => RFC2822,
|
33
|
+
"to" => RFC2822,
|
34
|
+
"cc" => RFC2822,
|
35
|
+
"bcc" => RFC2822,
|
36
|
+
"message-id" => RFC2822,
|
37
|
+
"in-reply-to" => RFC2822,
|
38
|
+
"references" => RFC2822,
|
39
|
+
# "subject" => RFC2822,
|
40
|
+
# "comments" => RFC2822,
|
41
|
+
"keywords" => RFC2822,
|
42
|
+
"resent-date" => RFC2822,
|
43
|
+
"resent-from" => RFC2822,
|
44
|
+
"resent-sender" => RFC2822,
|
45
|
+
"resent-to" => RFC2822,
|
46
|
+
"resent-cc" => RFC2822,
|
47
|
+
"resent-bcc" => RFC2822,
|
48
|
+
"resent-message-id" => RFC2822,
|
49
|
+
"return-path" => RFC2822,
|
50
|
+
"received" => RFC2822,
|
51
|
+
"content-type" => RFC2045,
|
52
|
+
# "content-description" => RFC2045,
|
53
|
+
"content-transfer-encoding" => RFC2045,
|
54
|
+
"content-id" => RFC2045,
|
55
|
+
"mime-version" => RFC2045,
|
56
|
+
"content-disposition" => RFC2183,
|
57
|
+
}
|
58
|
+
|
59
|
+
# 単一のヘッダ
|
60
|
+
class HeaderItem
|
61
|
+
# name:: ヘッダ名(String)
|
62
|
+
# raw:: ヘッダ値(String)
|
63
|
+
# opt:: オプション(Hash)
|
64
|
+
# :decode_mime_header:: MIMEヘッダをデコードする
|
65
|
+
# :output_charset:: デコード出力文字コード(デフォルト: UTF-8)
|
66
|
+
# :strict:: RFC違反時に ParseError 例外を発生する
|
67
|
+
def initialize(name, raw, opt={})
|
68
|
+
@name = name
|
69
|
+
@raw = raw
|
70
|
+
@parsed = nil
|
71
|
+
@opt = opt
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :raw
|
75
|
+
|
76
|
+
# パースした結果オブジェクトを返す
|
77
|
+
def parse()
|
78
|
+
return @parsed if @parsed
|
79
|
+
if HEADER_PARSER.key? @name then
|
80
|
+
begin
|
81
|
+
@parsed = HEADER_PARSER[@name].parse(@name, @raw, @opt)
|
82
|
+
rescue ParseError
|
83
|
+
raise if @opt[:strict]
|
84
|
+
@parsed = Loose.parse(@name, @raw, @opt)
|
85
|
+
end
|
86
|
+
else
|
87
|
+
r = @raw.chomp.gsub(/\s+/, " ")
|
88
|
+
if @opt[:decode_mime_header] then
|
89
|
+
@parsed = RFC2047.decode(r, @opt)
|
90
|
+
else
|
91
|
+
@parsed = r
|
92
|
+
end
|
93
|
+
end
|
94
|
+
class <<@parsed
|
95
|
+
attr_accessor :raw
|
96
|
+
end
|
97
|
+
@parsed.raw = @raw
|
98
|
+
|
99
|
+
# Content-Type, Content-Disposition parameter for RFC2231
|
100
|
+
if ["content-type", "content-disposition"].include? @name
|
101
|
+
new = RFC2231.parse_param @parsed.params, @opt
|
102
|
+
@parsed.params.replace new
|
103
|
+
end
|
104
|
+
|
105
|
+
return @parsed
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# 同じ名前を持つヘッダの集まり
|
110
|
+
class Header
|
111
|
+
def initialize(opt={})
|
112
|
+
@hash = {}
|
113
|
+
@parsed = {}
|
114
|
+
@raw = {}
|
115
|
+
@opt = opt
|
116
|
+
end
|
117
|
+
|
118
|
+
# name ヘッダに body を追加する
|
119
|
+
# name:: ヘッダ名(String)
|
120
|
+
# body:: ヘッダ値(String)
|
121
|
+
def add(name, body)
|
122
|
+
name = name.downcase
|
123
|
+
@hash[name] = [] unless @hash.key? name
|
124
|
+
@hash[name] << HeaderItem.new(name, body, @opt)
|
125
|
+
end
|
126
|
+
|
127
|
+
# パースした結果オブジェクトの配列を返す
|
128
|
+
# name:: ヘッダ名(String)
|
129
|
+
def [](name)
|
130
|
+
return nil unless @hash.key? name
|
131
|
+
return @parsed[name] if @parsed.key? name
|
132
|
+
@parsed[name] = @hash[name].map{|h| h.parse}.compact
|
133
|
+
return @parsed[name]
|
134
|
+
end
|
135
|
+
|
136
|
+
# 生ヘッダ値文字列の配列を返す
|
137
|
+
# name:: ヘッダ名(String)
|
138
|
+
def raw(name)
|
139
|
+
return nil unless @hash.key? name
|
140
|
+
return @raw[name] if @raw.key? name
|
141
|
+
@raw[name] = @hash[name].map{|h| h.raw}
|
142
|
+
return @raw[name]
|
143
|
+
end
|
144
|
+
|
145
|
+
# ヘッダ名の配列を返す
|
146
|
+
def keys()
|
147
|
+
return @hash.keys
|
148
|
+
end
|
149
|
+
|
150
|
+
# ヘッダが存在するか?
|
151
|
+
def key?(name)
|
152
|
+
return @hash.key?(name)
|
153
|
+
end
|
154
|
+
|
155
|
+
# 各ヘッダについてブロックを繰り返す
|
156
|
+
# ブロック引数は、[ヘッダ名, パース結果オブジェクト,...]]
|
157
|
+
def each()
|
158
|
+
@hash.each do |k, v|
|
159
|
+
yield k, self[k]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# メール全体またはひとつのパートを表すクラス
|
165
|
+
class Message
|
166
|
+
# src からヘッダ部を読み込み Header オブジェクトに保持する
|
167
|
+
# src:: gets メソッドを持つオブジェクト(ex. IO, StringIO)
|
168
|
+
# opt:: オプション(Hash)
|
169
|
+
# :skip_body:: 本文をスキップする
|
170
|
+
# :text_body_only:: text/* type 以外の本文をスキップする
|
171
|
+
# :extract_message_type:: message/* type を展開する
|
172
|
+
# :decode_mime_header:: MIMEヘッダをデコードする
|
173
|
+
# :decode_mime_filename:: ファイル名を MIME デコードする
|
174
|
+
# :output_charset:: デコード出力文字コード(デフォルト: 変換しない)
|
175
|
+
# :strict:: RFC違反時に ParseError 例外を発生する
|
176
|
+
# :keep_raw:: 生メッセージを保持する
|
177
|
+
# :charset_converter:: 文字コード変換用 Proc または Method
|
178
|
+
# :use_file:: body, raw がこのサイズを超えたらメモリではなくファイルを使用する
|
179
|
+
# boundary:: このパートの終わりを表す文字列の配列
|
180
|
+
def initialize(src, opt={}, boundary=[])
|
181
|
+
src = src.is_a?(String) ? StringIO.new(src) : src
|
182
|
+
@dio = DelimIO.new(src, boundary, opt[:keep_raw], opt[:use_file])
|
183
|
+
@opt = opt
|
184
|
+
@boundary = boundary
|
185
|
+
@from = @to = @cc = @subject = nil
|
186
|
+
@type = @subtype = @charset = @content_transfer_encoding = @filename = nil
|
187
|
+
@rawheader = ''
|
188
|
+
@message = nil
|
189
|
+
@body = @body_preconv = DataBuffer.new(opt[:use_file])
|
190
|
+
@part = []
|
191
|
+
opt[:charset_converter] ||= ConvCharset.method(:conv_charset)
|
192
|
+
|
193
|
+
read_header
|
194
|
+
read_body
|
195
|
+
read_part
|
196
|
+
end
|
197
|
+
|
198
|
+
attr_reader :header, :part, :message
|
199
|
+
|
200
|
+
def body
|
201
|
+
@body.str
|
202
|
+
end
|
203
|
+
|
204
|
+
def body_preconv
|
205
|
+
@body_preconv.str
|
206
|
+
end
|
207
|
+
|
208
|
+
# From ヘッダがあれば Mailbox を返す。
|
209
|
+
# なければ nil
|
210
|
+
def from()
|
211
|
+
return @from if @from
|
212
|
+
if @header.key? "from" then
|
213
|
+
@from = @header["from"][0][0]
|
214
|
+
else
|
215
|
+
@from = nil
|
216
|
+
end
|
217
|
+
return @from
|
218
|
+
end
|
219
|
+
|
220
|
+
# To ヘッダがあれば Mailbox の配列を返す
|
221
|
+
# なければ空配列
|
222
|
+
def to()
|
223
|
+
return @to if @to
|
224
|
+
if @header.key? "to" then
|
225
|
+
@to = @header["to"].flatten
|
226
|
+
else
|
227
|
+
@to = []
|
228
|
+
end
|
229
|
+
return @to
|
230
|
+
end
|
231
|
+
|
232
|
+
# Cc ヘッダがあれば Mailbox の配列を返す
|
233
|
+
# なければ空配列
|
234
|
+
def cc()
|
235
|
+
return @cc if @cc
|
236
|
+
if @header.key? "cc" then
|
237
|
+
@cc = @header["cc"].flatten
|
238
|
+
else
|
239
|
+
@cc = []
|
240
|
+
end
|
241
|
+
return @cc
|
242
|
+
end
|
243
|
+
|
244
|
+
# Subject ヘッダがあれば文字列を返す
|
245
|
+
# なければ空文字
|
246
|
+
def subject()
|
247
|
+
return @subject if @subject
|
248
|
+
if @header.key? "subject" then
|
249
|
+
@subject = @header["subject"].join(" ")
|
250
|
+
else
|
251
|
+
@subject = ""
|
252
|
+
end
|
253
|
+
return @subject
|
254
|
+
end
|
255
|
+
|
256
|
+
# Content-Type の type を返す。
|
257
|
+
# Content-Type がない場合は "text"
|
258
|
+
def type()
|
259
|
+
return @type if @type
|
260
|
+
if @header.key? "content-type" then
|
261
|
+
@type = @header["content-type"][0].type
|
262
|
+
else
|
263
|
+
@type = "text"
|
264
|
+
end
|
265
|
+
return @type
|
266
|
+
end
|
267
|
+
|
268
|
+
# Content-Type の subtype を返す。
|
269
|
+
# Content-Type がない場合は "plain"
|
270
|
+
def subtype()
|
271
|
+
return @subtype if @subtype
|
272
|
+
if @header.key? "content-type" then
|
273
|
+
@subtype = @header["content-type"][0].subtype
|
274
|
+
else
|
275
|
+
@subtype = "plain"
|
276
|
+
end
|
277
|
+
return @subtype
|
278
|
+
end
|
279
|
+
|
280
|
+
# Content-Type の charset 属性の値(小文字)を返す。
|
281
|
+
# charset 属性がない場合は nil
|
282
|
+
def charset()
|
283
|
+
return @charset if @charset
|
284
|
+
if @header.key? "content-type" then
|
285
|
+
c = @header["content-type"][0].params["charset"]
|
286
|
+
@charset = c && c.downcase
|
287
|
+
else
|
288
|
+
@charset = nil
|
289
|
+
end
|
290
|
+
return @charset
|
291
|
+
end
|
292
|
+
|
293
|
+
# マルチパートメッセージかどうかを返す
|
294
|
+
def multipart?()
|
295
|
+
return type == "multipart"
|
296
|
+
end
|
297
|
+
|
298
|
+
# Content-Transfer-Encoding の mechanism を返す
|
299
|
+
# Content-Transfer-Encoding がない場合は "7bit"
|
300
|
+
def content_transfer_encoding()
|
301
|
+
return @content_transfer_encoding if @content_transfer_encoding
|
302
|
+
if @header.key? "content-transfer-encoding" then
|
303
|
+
@content_transfer_encoding = @header["content-transfer-encoding"][0].mechanism
|
304
|
+
else
|
305
|
+
@content_transfer_encoding = "7bit"
|
306
|
+
end
|
307
|
+
return @content_transfer_encoding
|
308
|
+
end
|
309
|
+
|
310
|
+
# ファイル名を返す。
|
311
|
+
# Content-Disposition の filename パラメータ
|
312
|
+
# または Content-Type の name パラメータ。
|
313
|
+
# デフォルトは nil。
|
314
|
+
def filename()
|
315
|
+
return @filename if @filename
|
316
|
+
if @header.key? "content-disposition" and @header["content-disposition"][0].params.key? "filename" then
|
317
|
+
@filename = @header["content-disposition"][0].params["filename"]
|
318
|
+
elsif @header.key? "content-type" and @header["content-type"][0].params.key? "name" then
|
319
|
+
@filename = @header["content-type"][0].params["name"]
|
320
|
+
end
|
321
|
+
@filename = RFC2047.decode(@filename, @opt) if @opt[:decode_mime_filename] and @filename
|
322
|
+
return @filename
|
323
|
+
end
|
324
|
+
|
325
|
+
# 生メッセージを返す
|
326
|
+
def raw
|
327
|
+
@dio.keep_buffer.str
|
328
|
+
end
|
329
|
+
|
330
|
+
# 生ヘッダを返す
|
331
|
+
def rawheader
|
332
|
+
@rawheader
|
333
|
+
end
|
334
|
+
|
335
|
+
private
|
336
|
+
|
337
|
+
# ヘッダ部をパースする
|
338
|
+
# return:: true: 継続行あり
|
339
|
+
def read_header()
|
340
|
+
@header = Header.new(@opt)
|
341
|
+
headers = []
|
342
|
+
@dio.each_line do |line|
|
343
|
+
break if line.chomp.empty?
|
344
|
+
cont = line =~ /^[ \t]/
|
345
|
+
if (cont and headers.empty?) or (!cont and !line.include? ":") then
|
346
|
+
@dio.ungets
|
347
|
+
break
|
348
|
+
end
|
349
|
+
if line =~ /^[ \t]/ then
|
350
|
+
headers[-1] += line # :keep_raw 時の行破壊を防ぐため`<<'は使わない
|
351
|
+
else
|
352
|
+
headers << line
|
353
|
+
end
|
354
|
+
@rawheader << line
|
355
|
+
end
|
356
|
+
headers.each do |h|
|
357
|
+
name, body = h.split(/\s*:\s*/n, 2)
|
358
|
+
@header.add(name, body)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# 本文を読む
|
363
|
+
def read_body()
|
364
|
+
return if type == "multipart" or @dio.eof?
|
365
|
+
unless @opt[:extract_message_type] and type == "message" then
|
366
|
+
if @opt[:skip_body] or (@opt[:text_body_only] and type != "text")
|
367
|
+
@dio.each_line{} # 本文skip
|
368
|
+
return
|
369
|
+
end
|
370
|
+
end
|
371
|
+
body = ''
|
372
|
+
@dio.each_line do |line|
|
373
|
+
body << line
|
374
|
+
end
|
375
|
+
body.chomp! unless @dio.real_eof?
|
376
|
+
case content_transfer_encoding
|
377
|
+
when "quoted-printable" then @body << RFC2045.qp_decode(body)
|
378
|
+
when "base64" then @body << RFC2045.b64_decode(body)
|
379
|
+
when "uuencode", "x-uuencode", "x-uue" then @body << decode_uuencode(body)
|
380
|
+
else @body << body
|
381
|
+
end
|
382
|
+
@body_preconv = @body
|
383
|
+
if type == 'text' and charset and @opt[:output_charset] then
|
384
|
+
new_body = DataBuffer.new(@opt[:use_file])
|
385
|
+
begin
|
386
|
+
if @opt[:use_file] and @body.size > @opt[:use_file]
|
387
|
+
newline = @opt[:charset_converter].call(@opt[:output_charset], charset, "\n")
|
388
|
+
@body.io.each_line(newline) do |line|
|
389
|
+
new_body << @opt[:charset_converter].call(charset, @opt[:output_charset], line)
|
390
|
+
end
|
391
|
+
else
|
392
|
+
new_body << @opt[:charset_converter].call(charset, @opt[:output_charset], @body.str)
|
393
|
+
end
|
394
|
+
@body = new_body
|
395
|
+
rescue
|
396
|
+
# ignore
|
397
|
+
end
|
398
|
+
end
|
399
|
+
if @opt[:extract_message_type] and type == "message" and not @body.empty? then
|
400
|
+
@message = Message.new(@body.io, @opt)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# 各パートの Message オブジェクトの配列を作成
|
405
|
+
def read_part()
|
406
|
+
return if type != "multipart" or @dio.eof?
|
407
|
+
b = @header["content-type"][0].params["boundary"]
|
408
|
+
bd = ["--#{b}--", "--#{b}"]
|
409
|
+
last_line = @dio.each_line(bd){} # skip preamble
|
410
|
+
while last_line and last_line.chomp == bd.last
|
411
|
+
m = Message.new @dio, @opt, @boundary+bd
|
412
|
+
@part << m
|
413
|
+
last_line = @dio.gets # read boundary
|
414
|
+
end
|
415
|
+
@dio.each_line{} # skip epilogue
|
416
|
+
end
|
417
|
+
|
418
|
+
# uuencode のデコード
|
419
|
+
def decode_uuencode(str)
|
420
|
+
ret = ""
|
421
|
+
str.each_line do |line|
|
422
|
+
line.chomp!
|
423
|
+
next if line =~ /\A\s*\z/
|
424
|
+
next if line =~ /\Abegin \d\d\d [^ ]/
|
425
|
+
break if line =~ /\Aend\z/
|
426
|
+
ret.concat line.unpack("u").first
|
427
|
+
end
|
428
|
+
ret
|
429
|
+
end
|
430
|
+
|
431
|
+
# str をそのまま返す
|
432
|
+
def decode_plain(str)
|
433
|
+
str
|
434
|
+
end
|
435
|
+
|
436
|
+
end
|
437
|
+
|
438
|
+
# 特定の行を EOF とみなして gets が動く IO モドキ
|
439
|
+
class DelimIO
|
440
|
+
# src:: IO または StringIO
|
441
|
+
# delim:: 区切り行の配列
|
442
|
+
# keep:: 全行保存
|
443
|
+
# use_file:: keep_buffer がこのサイズを超えたらメモリではなくファイルを使用する
|
444
|
+
def initialize(src, delim=nil, keep=false, use_file=nil)
|
445
|
+
@src = src
|
446
|
+
@delim_re = delim && !delim.empty? && Regexp.new(delim.map{|d|"\\A#{Regexp.quote(d)}\\r?\\Z"}.join("|"))
|
447
|
+
@keep = keep
|
448
|
+
@keep_buffer = DataBuffer.new(use_file)
|
449
|
+
@line_buffer = nil
|
450
|
+
@eof = false # delim に達したら真
|
451
|
+
@real_eof = false
|
452
|
+
@last_read_line = nil
|
453
|
+
end
|
454
|
+
|
455
|
+
attr_reader :keep_buffer
|
456
|
+
|
457
|
+
# 行毎にブロックを繰り返す。
|
458
|
+
# delim に一致した場合は中断
|
459
|
+
# delim:: 区切り文字列の配列
|
460
|
+
# return:: delimに一致した行 or nil(EOFに達した)
|
461
|
+
def each_line(delim=nil)
|
462
|
+
return if @eof
|
463
|
+
while line = gets
|
464
|
+
return line if delim and delim.include? line.chomp
|
465
|
+
yield line
|
466
|
+
end
|
467
|
+
nil
|
468
|
+
end
|
469
|
+
alias each each_line
|
470
|
+
|
471
|
+
# 1行読み込む。@delim_re に一致する行で EOF
|
472
|
+
def gets
|
473
|
+
return if @eof
|
474
|
+
if @line_buffer
|
475
|
+
line = @line_buffer
|
476
|
+
@line_buffer = nil
|
477
|
+
else
|
478
|
+
line = @src.gets
|
479
|
+
unless line # EOF
|
480
|
+
@keep_buffer << @last_read_line if @keep and @last_read_line
|
481
|
+
@eof = @real_eof = true
|
482
|
+
return
|
483
|
+
end
|
484
|
+
end
|
485
|
+
if @delim_re and @delim_re.match line
|
486
|
+
@keep_buffer << @last_read_line if @keep and @last_read_line
|
487
|
+
@src.ungets
|
488
|
+
@eof = true
|
489
|
+
return
|
490
|
+
end
|
491
|
+
@keep_buffer << @last_read_line if @keep and @last_read_line
|
492
|
+
@last_read_line = line
|
493
|
+
line
|
494
|
+
end
|
495
|
+
|
496
|
+
def ungets
|
497
|
+
raise "preread line nothing" unless @last_read_line
|
498
|
+
@eof = false
|
499
|
+
@line_buffer = @last_read_line
|
500
|
+
@last_read_line = nil
|
501
|
+
end
|
502
|
+
|
503
|
+
def eof?
|
504
|
+
@eof
|
505
|
+
end
|
506
|
+
|
507
|
+
def real_eof?
|
508
|
+
@src.is_a?(DelimIO) ? @src.real_eof? : @real_eof
|
509
|
+
end
|
510
|
+
|
511
|
+
end
|
512
|
+
|
513
|
+
# 通常はメモリにデータを保持し、それ以上はファイル(Tempfile)に保持するためのクラス
|
514
|
+
class DataBuffer
|
515
|
+
# limit:: データがこのバイト数を超えたらファイルに保持する。nil の場合は無制限。
|
516
|
+
def initialize(limit)
|
517
|
+
@limit = limit
|
518
|
+
@buffer = StringIO.new
|
519
|
+
end
|
520
|
+
|
521
|
+
# バッファに文字列を追加する
|
522
|
+
def <<(str)
|
523
|
+
if @limit and @buffer.is_a? StringIO and @buffer.size+str.size > @limit
|
524
|
+
file = Tempfile.new 'mailparser_databuffer'
|
525
|
+
file.unlink rescue nil
|
526
|
+
file.write @buffer.string
|
527
|
+
@buffer = file
|
528
|
+
end
|
529
|
+
@buffer << str
|
530
|
+
end
|
531
|
+
|
532
|
+
# バッファ内のデータを返す
|
533
|
+
def str
|
534
|
+
if @buffer.is_a? StringIO
|
535
|
+
@buffer.string
|
536
|
+
else
|
537
|
+
@buffer.rewind
|
538
|
+
@buffer.read
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
# IOオブジェクト(のようなもの)を返す
|
543
|
+
def io
|
544
|
+
@buffer.rewind
|
545
|
+
@buffer
|
546
|
+
end
|
547
|
+
|
548
|
+
# データの大きさを返す
|
549
|
+
def size
|
550
|
+
@buffer.pos
|
551
|
+
end
|
552
|
+
|
553
|
+
# バッファが空かどうかを返す
|
554
|
+
def empty?
|
555
|
+
@buffer.pos == 0
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|