internet_message 0.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.
@@ -0,0 +1,457 @@
1
+ require 'date'
2
+ require 'base64'
3
+ require 'mmapscanner'
4
+
5
+ class InternetMessage
6
+ dir = File.dirname __FILE__
7
+ require "#{dir}/internet_message/header_field"
8
+ require "#{dir}/internet_message/mailbox"
9
+ require "#{dir}/internet_message/group"
10
+ require "#{dir}/internet_message/message_id"
11
+ require "#{dir}/internet_message/received"
12
+ require "#{dir}/internet_message/content_type"
13
+ require "#{dir}/internet_message/content_disposition"
14
+
15
+ # @param [File, MmapScanner, String] src message source
16
+ # @param [Hash] opt option
17
+ # @option opt [boolean] :decode_mime_header(nil) to decode RFC2047 mime header
18
+ def initialize(src, opt={})
19
+ @src = MmapScanner.new(src)
20
+ @opt = opt
21
+ @parsed = @parse_multipart = false
22
+ @preamble = @epilogue = nil
23
+ @parts = []
24
+ @rawheader = @rawbody = nil
25
+ @decode_mime_header = opt[:decode_mime_header]
26
+ @fields = []
27
+ @field = Hash.new{|h,k| h[k] = []}
28
+ end
29
+
30
+ # To close object. After close, don't use this object.
31
+ def close
32
+ if @src.data.respond_to? :unmap
33
+ @src.data.unmap
34
+ end
35
+ end
36
+
37
+ # @return [Array of HeaderField] Header fields
38
+ def fields
39
+ parse_header
40
+ @fields
41
+ end
42
+
43
+ # @return [Hash] 'field-name' => [HeaderField, ...]
44
+ def field
45
+ parse_header
46
+ @field
47
+ end
48
+
49
+ defm = proc do |mname|
50
+ fname = mname.to_s.gsub(/_/, '-')
51
+ define_method mname do
52
+ f = field[fname].first
53
+ f && f.parse
54
+ end
55
+ end
56
+
57
+ # @method date
58
+ # @return [DateTime] Date field
59
+ defm.call :date
60
+
61
+ # @method message_id
62
+ # @return [MessageId] Message-Id field
63
+ defm.call :message_id
64
+
65
+ # @method mime_version
66
+ # @return [String] Mime-Version field
67
+ defm.call :mime_version
68
+
69
+ # @method content_transfer_encoding
70
+ # @return [String] Content-Transfer-Encoding field
71
+ defm.call :content_transfer_encoding
72
+
73
+ # @method content_id
74
+ # @return [MessageId] Content-Id field
75
+ defm.call :content_id
76
+
77
+ # @method content_type
78
+ # @return [ContentType] Content-Type field
79
+ defm.call :content_type
80
+
81
+ # @method content_disposition
82
+ # @return [ContentDisposition] Content-Dispositoin field
83
+ defm.call :content_disposition
84
+
85
+ # @return [Mailbox] From field
86
+ def from
87
+ f = field['from'].first
88
+ f && f.parse(@decode_mime_header).first
89
+ end
90
+
91
+ defm = proc do |mname|
92
+ fname = mname.to_s.gsub(/_/, '-')
93
+ define_method mname do
94
+ f = field[fname].first
95
+ f && f.parse(@decode_mime_header)
96
+ end
97
+ end
98
+
99
+ # @method sender
100
+ # @return [Mailbox] Sender field
101
+ defm.call :sender
102
+
103
+ # @method subject
104
+ # @return [String] Subject field
105
+ defm.call :subject
106
+
107
+ # @method content_description
108
+ # @return [String] Content-Description field
109
+ defm.call :content_description
110
+
111
+ defm = proc do |mname|
112
+ fname = mname.to_s.gsub(/_/, '-')
113
+ define_method mname do
114
+ f = field[fname].first
115
+ f && f.parse || []
116
+ end
117
+ end
118
+
119
+ # @method in_reply_to
120
+ # @return [Array of MessageId] In-Reply-To field
121
+ defm.call :in_reply_to
122
+
123
+ # @method references
124
+ # @return [Array of MessageId] References field
125
+ defm.call :references
126
+
127
+ defm = proc do |mname|
128
+ fname = mname.to_s.gsub(/_/, '-')
129
+ define_method mname do
130
+ f = field[fname].first
131
+ f && f.parse(@decode_mime_header) || []
132
+ end
133
+ end
134
+
135
+ # @method reply_to
136
+ # @return [Array of Mailbox/Group] Reply-To field
137
+ defm.call :reply_to
138
+
139
+ # @method to
140
+ # @return [Array of Mailbox/Group] To field
141
+ defm.call :to
142
+
143
+ # @method cc
144
+ # @return [Array of Mailbox/Group] Cc field
145
+ defm.call :cc
146
+
147
+ # @method bcc
148
+ # @return [Array of Mailbox/Group] Bcc field
149
+ defm.call :bcc
150
+
151
+ # @return [Array of String] Comments field
152
+ def comments
153
+ field['comments'].map{|f| f.parse(@decode_mime_header)}
154
+ end
155
+
156
+ # @return [Array of String] Keywords field
157
+ def keywords
158
+ field['keywords'].map{|f| f.parse(@decode_mime_header)}.flatten
159
+ end
160
+
161
+ # @return [TraceBlockList] trace block list
162
+ def trace_blocks
163
+ parse_header
164
+ @trace_blocks
165
+ end
166
+
167
+ defm = proc do |mname|
168
+ define_method mname do
169
+ trace = trace_blocks.first
170
+ trace && trace.method(mname).call
171
+ end
172
+ end
173
+
174
+ # @method resent_date
175
+ # @return [DateTime] Resent-Date field of first trace block
176
+ defm.call :resent_date
177
+
178
+ # @method resent_from
179
+ # @return [Mailbox] Resent-From field of first trace block
180
+ defm.call :resent_from
181
+
182
+ # @method resent_sender
183
+ # @return [Mailbox] Resent-Sender field of first trace block
184
+ defm.call :resent_sender
185
+
186
+ # @method resent_message_id
187
+ # @return [MessageId] Resent-Message-Id field of first trace block
188
+ defm.call :resent_message_id
189
+
190
+ # @method return_path
191
+ # @return [Address] Return-Path field of first trace block
192
+ defm.call :return_path
193
+
194
+ defm = proc do |mname|
195
+ define_method mname do
196
+ trace = trace_blocks.first
197
+ trace ? trace.method(mname).call : []
198
+ end
199
+ end
200
+
201
+ # @method resent_to
202
+ # @return [Array of Mailbox/Group] Resent-To field of first trace block
203
+ defm.call :resent_to
204
+
205
+ # @method resent_cc
206
+ # @return [Array of Mailbox/Group] Resent-Cc field of first trace block
207
+ defm.call :resent_cc
208
+
209
+ # @method resent_bcc
210
+ # @return [Array of Mailbox/Group] Resent-Bcc field of first trace block
211
+ defm.call :resent_bcc
212
+
213
+ # @method received
214
+ # @return [Received] Received field of first trace block
215
+ defm.call :received
216
+
217
+ # @return [String] media type. 'text' if Content-Type field doesn't exists.
218
+ def type
219
+ content_type ? content_type.type : 'text'
220
+ end
221
+
222
+ # @return [String] media subtype. 'plain' if Content-Type field doesn't exists.
223
+ def subtype
224
+ content_type ? content_type.subtype : 'plain'
225
+ end
226
+
227
+ # @return [String] charset attribute. 'us-ascii' if Content-Type field doesn't exists.
228
+ def charset
229
+ (content_type && content_type.attribute['charset']) || 'us-ascii'
230
+ end
231
+
232
+ # @return [String] body text.
233
+ def body
234
+ parse_header
235
+ s = @rawbody.to_s
236
+ case content_transfer_encoding.to_s.downcase
237
+ when 'base64'
238
+ s = Base64.decode64 s
239
+ when 'quoted-printable'
240
+ s = s.unpack('M').join
241
+ end
242
+ s.force_encoding(charset) rescue s
243
+ end
244
+
245
+ # @return [String] preamble of multiple part message. nil if single part message.
246
+ def preamble
247
+ parse_multipart
248
+ @preamble
249
+ end
250
+
251
+ # @return [String] epilogue of multiple part message. nil if single part message.
252
+ def epilogue
253
+ parse_multipart
254
+ @epilogue
255
+ end
256
+
257
+ # @return [Array of InternetMessage] parts of multiple part message. empty if single part message.
258
+ def parts
259
+ parse_multipart
260
+ @parts
261
+ end
262
+
263
+ # @return [InternetMessage] message if Content-Type is 'message/*'.
264
+ def message
265
+ type == 'message' ? InternetMessage.new(@rawbody, @opt) : nil
266
+ end
267
+
268
+ # @private
269
+ def self.decode_mime_header_str(str)
270
+ decode_mime_header_words(str.split(/([ \t\r\n]+)/, -1))
271
+ end
272
+
273
+ # @private
274
+ def self.decode_mime_header_words(words)
275
+ ret = ''
276
+ after_mime = nil
277
+ prev_sp = ' '
278
+ words.each do |word|
279
+ s = word.to_s
280
+ if s =~ /\A[ \t\r\n]+\z/
281
+ prev_sp = s
282
+ next
283
+ end
284
+ if (word.is_a?(Token) ? word.type == :TOKEN : true) && s =~ /\A=\?([^?]+)\?([bq])\?([^?]+)\?=\z/i
285
+ charset, enc, data = $1, $2, $3
286
+ if enc.downcase == 'b'
287
+ data = Base64.decode64(data)
288
+ else
289
+ data = data.gsub(/_/,' ').unpack('M').join
290
+ end
291
+ data = data.force_encoding(charset) rescue data
292
+ ret.concat prev_sp if after_mime == false
293
+ ret.concat data.encode(Encoding::UTF_8, :invalid=>:replace, :undef=>:replace)
294
+ after_mime = true
295
+ else
296
+ ret.concat prev_sp unless after_mime.nil?
297
+ ret.concat s
298
+ after_mime = false
299
+ end
300
+ end
301
+ ret
302
+ end
303
+
304
+ private
305
+
306
+ # @private
307
+ def split_header_body
308
+ @rawheader = @src.scan_until(/(?=^\r?\n)|\z/)
309
+ @src.skip(/\r?\n/) # skip delimiter
310
+ @rawbody = @src.rest
311
+ end
312
+
313
+ # @private
314
+ def parse_header
315
+ return if @parsed
316
+ split_header_body
317
+ @trace_blocks = TraceBlockList.new
318
+ while line = @rawheader.scan(/.*(\r?\n[ \t].*)*(?=\r?\n|\z)/n)
319
+ if line.skip(/(.*?):[ \t]*/)
320
+ field_name = line.matched(1).to_s.downcase
321
+ field_value = line.rest
322
+ field = HeaderField.new(field_name, field_value, line)
323
+ @fields.push field
324
+ @field[field_name].push field
325
+ @trace_blocks.push field
326
+ end
327
+ @rawheader.skip(/\r?\n/)
328
+ end
329
+ @trace_blocks.clean
330
+ @parsed = true
331
+ end
332
+
333
+ # @private
334
+ def parse_multipart
335
+ return if @parse_multipart
336
+ return unless content_type
337
+ boundary = content_type.attribute['boundary']
338
+ return unless boundary
339
+ b_re = Regexp.escape boundary
340
+ @rawbody.skip(/(.*?)^--#{b_re}(\r?\n|\z)/nm) or return
341
+ @preamble = @rawbody.matched(1).to_s.chomp
342
+ @parts = []
343
+ last = false
344
+ until last
345
+ @rawbody.skip(/(.*?)\r?\n--#{b_re}(--)?(\r?\n|\z)/nm) or break
346
+ @parts.push InternetMessage.new(@rawbody.matched(1))
347
+ last = true if @rawbody.matched(2)
348
+ end
349
+ @epilogue = @rawbody.rest.to_s
350
+ @parse_multipart = true
351
+ end
352
+
353
+ class TraceBlockList
354
+ include Enumerable
355
+
356
+ attr_reader :blocks
357
+
358
+ # @private
359
+ def initialize
360
+ @block = TraceBlock.new
361
+ @blocks = [@block]
362
+ @state = nil
363
+ end
364
+
365
+ # @private
366
+ def push(field)
367
+ case field.name
368
+ when 'return-path'
369
+ @block = TraceBlock.new
370
+ @blocks.push @block
371
+ @block.push field
372
+ @state = :return
373
+ when 'received'
374
+ unless @state == :return or @state == :received
375
+ @block = TraceBlock.new
376
+ @blocks.push @block
377
+ end
378
+ @block.push field
379
+ @state = :received
380
+ when /\Aresent-/
381
+ unless @state == :return or @state == :received or @state == :resent
382
+ @block = TraceBlock.new
383
+ @blocks.push @block
384
+ end
385
+ @block.push field
386
+ @state = :resent
387
+ end
388
+ end
389
+
390
+ # @private
391
+ def clean
392
+ @blocks.delete_if(&:empty?)
393
+ end
394
+
395
+ def each
396
+ @blocks.each do |b|
397
+ yield b
398
+ end
399
+ end
400
+
401
+ end
402
+
403
+ class TraceBlock < Array
404
+ # @return [DateTime] Resent-Date field in trace block
405
+ def resent_date
406
+ f = self.find{|f| f.name == 'resent-date'}
407
+ f && f.parse
408
+ end
409
+
410
+ # @return [Mailbox] Resent-From field in trace block
411
+ def resent_from
412
+ f = self.find{|f| f.name == 'resent-from'}
413
+ f && f.parse(@decode_mime_header).first
414
+ end
415
+
416
+ # @return [Mailbox] Resent-Sender field in trace block
417
+ def resent_sender
418
+ f = self.find{|f| f.name == 'resent-sender'}
419
+ f && f.parse(@decode_mime_header)
420
+ end
421
+
422
+ # @return [Array of Mailbox/Group] Resent-To field in trace block
423
+ def resent_to
424
+ f = self.find{|f| f.name == 'resent-to'}
425
+ f ? f.parse(@decode_mime_header) : []
426
+ end
427
+
428
+ # @return [Array of Mailbox/Group] Resent-Cc field in trace block
429
+ def resent_cc
430
+ f = self.find{|f| f.name == 'resent-cc'}
431
+ f ? f.parse(@decode_mime_header) : []
432
+ end
433
+
434
+ # @return [Array of Mailbox/Group] Resent-Bcc field in trace block
435
+ def resent_bcc
436
+ f = self.find{|f| f.name == 'resent-bcc'}
437
+ f ? f.parse(@decode_mime_header) : []
438
+ end
439
+
440
+ # @return [MessageId] Resent-Message-Id field in trace block
441
+ def resent_message_id
442
+ f = self.find{|f| f.name == 'resent-message-id'}
443
+ f && f.parse
444
+ end
445
+
446
+ # @return [Address] Return-Path field in trace block
447
+ def return_path
448
+ f = self.find{|f| f.name == 'return-path'}
449
+ f && f.parse
450
+ end
451
+
452
+ # @return [Array of Received] Received fields in trace block
453
+ def received
454
+ self.select{|f| f.name == 'received'}.map{|f| f.parse}.compact
455
+ end
456
+ end
457
+ end
@@ -0,0 +1,34 @@
1
+ require __FILE__.sub(/\/spec\//, '/lib/').sub(/_spec\.rb\z/,'')
2
+
3
+ describe InternetMessage::Address do
4
+ describe '#to_s' do
5
+ let(:local_part){'hoge'}
6
+ let(:domain){'example.com'}
7
+ subject{InternetMessage::Address.new(local_part, domain)}
8
+ context 'simple address' do
9
+ it 'return mailaddress' do
10
+ subject.to_s.should == 'hoge@example.com'
11
+ end
12
+ end
13
+
14
+ context 'localpart including special character' do
15
+ let(:local_part){'hoge"fuga\\foo'}
16
+ it 'quote localpart' do
17
+ subject.to_s.should == '"hoge\\"fuga\\\\foo"@example.com'
18
+ end
19
+ end
20
+
21
+ context 'localpart including ".."' do
22
+ let(:local_part){'hoge..fuga'}
23
+ it 'quote localpart' do
24
+ subject.to_s.should == '"hoge..fuga"@example.com'
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#==' do
30
+ it 'is case-insensitive' do
31
+ InternetMessage::Address.new('hoge','example.com').should == InternetMessage::Address.new('HOGE', 'EXAMPLE.COM')
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ require __FILE__.sub(/\/spec\//, '/lib/').sub(/_spec\.rb\z/,'')
2
+
3
+ describe InternetMessage::Group do
4
+ describe '.parse' do
5
+ subject{InternetMessage::Group.parse(src)}
6
+
7
+ context "for 'group: hoge@example.com, fuga@example.net;'" do
8
+ let(:src){'group: hoge@example.com, fuga@example.net;'}
9
+ its(:display_name){should == 'group'}
10
+ its(:mailbox_list){should == [InternetMessage::Mailbox.new('hoge','example.com'), InternetMessage::Mailbox.new('fuga','example.net')]}
11
+ end
12
+
13
+ context "for 'group: hoge@example.com, fuga@example.net'" do
14
+ let(:src){'group: hoge@example.com, fuga@example.net'}
15
+ its(:display_name){should == 'group'}
16
+ its(:mailbox_list){should == [InternetMessage::Mailbox.new('hoge','example.com'), InternetMessage::Mailbox.new('fuga','example.net')]}
17
+ end
18
+
19
+ context "for 'group:;'" do
20
+ let(:src){'group:;'}
21
+ its(:display_name){should == 'group'}
22
+ its(:mailbox_list){should be_empty}
23
+ end
24
+
25
+ context "for 'group:'" do
26
+ let(:src){'group:'}
27
+ its(:display_name){should == 'group'}
28
+ its(:mailbox_list){should be_empty}
29
+ end
30
+
31
+ context "for ':;'" do
32
+ let(:src){':;'}
33
+ its(:display_name){should == ''}
34
+ its(:mailbox_list){should be_empty}
35
+ end
36
+
37
+ context "for ':'" do
38
+ let(:src){':'}
39
+ its(:display_name){should == ''}
40
+ its(:mailbox_list){should be_empty}
41
+ end
42
+
43
+ end
44
+
45
+ describe '#to_s' do
46
+ let(:display_name){'group'}
47
+ let(:mailbox_list){[double('Mailbox', :to_str=>'hoge@example.com'), double('Mailbox', :to_str=>'fuga@example.net')]}
48
+ subject{InternetMessage::Group.new(display_name, mailbox_list)}
49
+ it 'return group string' do
50
+ subject.to_s.should == 'group: hoge@example.com, fuga@example.net;'
51
+ end
52
+ context 'with display_name including special character' do
53
+ let(:display_name){'hoge.fuga,foo'}
54
+ it 'returns with display_name' do
55
+ subject.to_s.should == '"hoge.fuga,foo": hoge@example.com, fuga@example.net;'
56
+ end
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,138 @@
1
+ require __FILE__.sub(/\/spec\//, '/lib/').sub(/_spec\.rb\z/,'')
2
+
3
+ describe InternetMessage::Mailbox do
4
+ describe '.parse' do
5
+ subject{InternetMessage::Mailbox.parse(src)}
6
+
7
+ shared_examples_for 'hoge.fuga@example.com' do
8
+ its(:local_part){should == 'hoge.fuga'}
9
+ its(:domain){should == 'example.com'}
10
+ end
11
+
12
+ context "for 'hoge.fuga@example.com'" do
13
+ let(:src){'hoge.fuga@example.com'}
14
+ it_should_behave_like 'hoge.fuga@example.com'
15
+ end
16
+
17
+ context "for 'hoge.fuga@example.com (comment)'" do
18
+ let(:src){'hoge.fuga@example.com (comment)'}
19
+ it_should_behave_like 'hoge.fuga@example.com'
20
+ end
21
+
22
+ context "for '<hoge.fuga@example.com> (comment)'" do
23
+ let(:src){'<hoge.fuga@example.com> (comment)'}
24
+ it_should_behave_like 'hoge.fuga@example.com'
25
+ end
26
+
27
+ context "for 'hoge(a).fuga(b)@(c)example.com(d)'" do
28
+ let(:src){'hoge(a).fuga(b)@(c)example.com(d)'}
29
+ it_should_behave_like 'hoge.fuga@example.com'
30
+ end
31
+
32
+ context "for '\"hoge.fuga\"@example.com'" do
33
+ let(:src){'"hoge.fuga"@example.com'}
34
+ it_should_behave_like 'hoge.fuga@example.com'
35
+ end
36
+
37
+ context "for ' hoge . fuga @ example . com '" do
38
+ let(:src){' hoge . fuga @ example . com '}
39
+ it_should_behave_like 'hoge.fuga@example.com'
40
+ end
41
+
42
+ context "for 'hoge..fuga.@example..com'" do
43
+ let(:src){'hoge..fuga.@example..com'}
44
+ its(:local_part){should == 'hoge..fuga.'}
45
+ its(:domain){should == 'example..com'}
46
+ end
47
+
48
+ context "for '@example.com'" do
49
+ let(:src){'@example.com'}
50
+ its(:local_part){should == ''}
51
+ its(:domain){should == 'example.com'}
52
+ end
53
+
54
+ context "for 'hoge.fuga@'" do
55
+ let(:src){'hoge.fuga@'}
56
+ its(:local_part){should == 'hoge.fuga'}
57
+ its(:domain){should == ''}
58
+ end
59
+
60
+ context "for 'display <hoge.fuga@example.com>'" do
61
+ let(:src){'display <hoge.fuga@example.com>'}
62
+ it_should_behave_like 'hoge.fuga@example.com'
63
+ its(:display_name){should == 'display'}
64
+ end
65
+ end
66
+
67
+ describe '.parse_list' do
68
+ subject{InternetMessage::Mailbox.parse_list(src)}
69
+
70
+ context "for 'hoge.fuga@example.com, foo.bar@example.net'" do
71
+ let(:src){'hoge.fuga@example.com, foo.bar@example.net'}
72
+ it {should == [
73
+ InternetMessage::Mailbox.new('hoge.fuga', 'example.com'),
74
+ InternetMessage::Mailbox.new('foo.bar', 'example.net'),
75
+ ]
76
+ }
77
+ end
78
+
79
+ context "for '\"hoge,fuga\" <hoge.fuga@example.com>, \"foo,bar\" <foo.bar@example.net>'" do
80
+ let(:src){'"hoge,fuga" <hoge.fuga@example.com>, "foo,bar" <foo.bar@example.net>'}
81
+ it {should == [
82
+ InternetMessage::Mailbox.new('hoge.fuga', 'example.com', 'hoge,fuga'),
83
+ InternetMessage::Mailbox.new('foo.bar', 'example.net', 'foo,bar'),
84
+ ]
85
+ }
86
+ end
87
+
88
+ context "for ',hoge.fuga@example.com,,, foo.bar@example.net,'" do
89
+ let(:src){',hoge.fuga@example.com,,, foo.bar@example.net,'}
90
+ it {should == [
91
+ InternetMessage::Mailbox.new('hoge.fuga', 'example.com'),
92
+ InternetMessage::Mailbox.new('foo.bar', 'example.net'),
93
+ ]
94
+ }
95
+ end
96
+ end
97
+
98
+ describe '#to_s' do
99
+ let(:local_part){'hoge'}
100
+ let(:domain){'example.com'}
101
+ let(:display_name){nil}
102
+ subject{InternetMessage::Mailbox.new(local_part, domain, display_name)}
103
+ context 'simple address' do
104
+ it 'return mailaddress' do
105
+ subject.to_s.should == 'hoge@example.com'
106
+ end
107
+ end
108
+
109
+ context 'localpart including special character' do
110
+ let(:local_part){'hoge"fuga\\foo'}
111
+ it 'quote localpart' do
112
+ subject.to_s.should == '"hoge\\"fuga\\\\foo"@example.com'
113
+ end
114
+ end
115
+
116
+ context 'localpart including ".."' do
117
+ let(:local_part){'hoge..fuga'}
118
+ it 'quote localpart' do
119
+ subject.to_s.should == '"hoge..fuga"@example.com'
120
+ end
121
+ end
122
+
123
+ context 'with simple display_name' do
124
+ let(:display_name){'hoge fuga'}
125
+ it 'returns with display_name' do
126
+ subject.to_s.should == 'hoge fuga <hoge@example.com>'
127
+ end
128
+ end
129
+
130
+ context 'with display_name including special character' do
131
+ let(:display_name){'hoge.fuga,foo'}
132
+ it 'returns with display_name' do
133
+ subject.to_s.should == '"hoge.fuga,foo" <hoge@example.com>'
134
+ end
135
+ end
136
+ end
137
+ end
138
+