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