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.
- data/lib/internet_message/address.rb +35 -0
- data/lib/internet_message/content_attribute.rb +54 -0
- data/lib/internet_message/content_disposition.rb +37 -0
- data/lib/internet_message/content_type.rb +38 -0
- data/lib/internet_message/group.rb +49 -0
- data/lib/internet_message/header_field.rb +129 -0
- data/lib/internet_message/mailbox.rb +102 -0
- data/lib/internet_message/message_id.rb +47 -0
- data/lib/internet_message/received.rb +59 -0
- data/lib/internet_message/tokenizer.rb +61 -0
- data/lib/internet_message.rb +457 -0
- data/spec/internet_message/address_spec.rb +34 -0
- data/spec/internet_message/group_spec.rb +60 -0
- data/spec/internet_message/mailbox_spec.rb +138 -0
- data/spec/internet_message/received_spec.rb +49 -0
- data/spec/internet_message/tokenizer_spec.rb +59 -0
- data/spec/internet_message_spec.rb +573 -0
- metadata +74 -0
|
@@ -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
|
+
|