rmail 0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. data/NEWS +309 -0
  2. data/NOTES +14 -0
  3. data/README +83 -0
  4. data/THANKS +25 -0
  5. data/TODO +112 -0
  6. data/guide/Intro.txt +122 -0
  7. data/guide/MIME.txt +6 -0
  8. data/guide/TableOfContents.txt +13 -0
  9. data/install.rb +1023 -0
  10. data/lib/rmail.rb +50 -0
  11. data/lib/rmail/address.rb +829 -0
  12. data/lib/rmail/header.rb +987 -0
  13. data/lib/rmail/mailbox.rb +62 -0
  14. data/lib/rmail/mailbox/mboxreader.rb +182 -0
  15. data/lib/rmail/message.rb +201 -0
  16. data/lib/rmail/parser.rb +412 -0
  17. data/lib/rmail/parser/multipart.rb +217 -0
  18. data/lib/rmail/parser/pushbackreader.rb +173 -0
  19. data/lib/rmail/serialize.rb +190 -0
  20. data/lib/rmail/utils.rb +59 -0
  21. data/rmail.gemspec +17 -0
  22. data/tests/addrgrammar.txt +113 -0
  23. data/tests/data/mbox.odd +4 -0
  24. data/tests/data/mbox.simple +8 -0
  25. data/tests/data/multipart/data.1 +5 -0
  26. data/tests/data/multipart/data.10 +1 -0
  27. data/tests/data/multipart/data.11 +9 -0
  28. data/tests/data/multipart/data.12 +9 -0
  29. data/tests/data/multipart/data.13 +3 -0
  30. data/tests/data/multipart/data.14 +3 -0
  31. data/tests/data/multipart/data.15 +3 -0
  32. data/tests/data/multipart/data.16 +3 -0
  33. data/tests/data/multipart/data.17 +0 -0
  34. data/tests/data/multipart/data.2 +5 -0
  35. data/tests/data/multipart/data.3 +2 -0
  36. data/tests/data/multipart/data.4 +3 -0
  37. data/tests/data/multipart/data.5 +1 -0
  38. data/tests/data/multipart/data.6 +2 -0
  39. data/tests/data/multipart/data.7 +3 -0
  40. data/tests/data/multipart/data.8 +5 -0
  41. data/tests/data/multipart/data.9 +4 -0
  42. data/tests/data/parser.badmime1 +4 -0
  43. data/tests/data/parser.badmime2 +6 -0
  44. data/tests/data/parser.nested-multipart +75 -0
  45. data/tests/data/parser.nested-simple +12 -0
  46. data/tests/data/parser.nested-simple2 +16 -0
  47. data/tests/data/parser.nested-simple3 +21 -0
  48. data/tests/data/parser.rfc822 +65 -0
  49. data/tests/data/parser.simple-mime +24 -0
  50. data/tests/data/parser/multipart.1 +8 -0
  51. data/tests/data/parser/multipart.10 +4 -0
  52. data/tests/data/parser/multipart.11 +12 -0
  53. data/tests/data/parser/multipart.12 +12 -0
  54. data/tests/data/parser/multipart.13 +6 -0
  55. data/tests/data/parser/multipart.14 +6 -0
  56. data/tests/data/parser/multipart.15 +6 -0
  57. data/tests/data/parser/multipart.16 +6 -0
  58. data/tests/data/parser/multipart.2 +8 -0
  59. data/tests/data/parser/multipart.3 +5 -0
  60. data/tests/data/parser/multipart.4 +6 -0
  61. data/tests/data/parser/multipart.5 +4 -0
  62. data/tests/data/parser/multipart.6 +5 -0
  63. data/tests/data/parser/multipart.7 +6 -0
  64. data/tests/data/parser/multipart.8 +8 -0
  65. data/tests/data/parser/multipart.9 +7 -0
  66. data/tests/data/transparency/absolute.1 +5 -0
  67. data/tests/data/transparency/absolute.2 +1 -0
  68. data/tests/data/transparency/absolute.3 +2 -0
  69. data/tests/data/transparency/absolute.4 +3 -0
  70. data/tests/data/transparency/absolute.5 +4 -0
  71. data/tests/data/transparency/absolute.6 +49 -0
  72. data/tests/data/transparency/message.1 +73 -0
  73. data/tests/data/transparency/message.2 +34 -0
  74. data/tests/data/transparency/message.3 +63 -0
  75. data/tests/data/transparency/message.4 +5 -0
  76. data/tests/data/transparency/message.5 +15 -0
  77. data/tests/data/transparency/message.6 +1185 -0
  78. data/tests/runtests.rb +35 -0
  79. data/tests/testaddress.rb +1192 -0
  80. data/tests/testbase.rb +207 -0
  81. data/tests/testheader.rb +1207 -0
  82. data/tests/testmailbox.rb +47 -0
  83. data/tests/testmboxreader.rb +161 -0
  84. data/tests/testmessage.rb +257 -0
  85. data/tests/testparser.rb +634 -0
  86. data/tests/testparsermultipart.rb +205 -0
  87. data/tests/testpushbackreader.rb +40 -0
  88. data/tests/testserialize.rb +264 -0
  89. data/tests/testtestbase.rb +112 -0
  90. data/tests/testtranspparency.rb +105 -0
  91. metadata +143 -0
@@ -0,0 +1,987 @@
1
+ #--
2
+ # Copyright (c) 2001, 2002, 2003, 2004 Matt Armstrong. All rights
3
+ # reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # 2. Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote products
14
+ # derived from this software without specific prior written permission.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17
+ # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18
+ # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
19
+ # NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
21
+ # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
+ #
27
+ #++
28
+ # Implements the RMail::Header class.
29
+ require 'rmail/utils'
30
+ require 'rmail/address'
31
+ require 'digest/md5'
32
+ require 'time'
33
+
34
+ module RMail
35
+
36
+ # A class that supports the reading, writing and manipulation of
37
+ # RFC2822 mail headers.
38
+
39
+ # =Overview
40
+ #
41
+ # The RMail::Header class supports the creation and manipulation of
42
+ # RFC2822 mail headers.
43
+ #
44
+ # A mail header is a little bit like a Hash. The fields are keyed
45
+ # by a string field name. It is also a little bit like an Array,
46
+ # since the fields are in a specific order. This class provides
47
+ # many of the methods of both the Hash and Array class. It also
48
+ # includes the Enumerable module.
49
+ #
50
+ # =Terminology
51
+ #
52
+ # header:: The entire header. Each RMail::Header object is one
53
+ # mail header.
54
+ #
55
+ # field:: An element of the header. Fields have a name and a value.
56
+ # For example, the field "Subject: Hi Mom!" has a name of
57
+ # "Subject" and a value of "Hi Mom!"
58
+ #
59
+ # name:: A name of a field. For example: "Subject" or "From".
60
+ #
61
+ # value:: The value of a field.
62
+ #
63
+ # =Conventions
64
+ #
65
+ # The header's fields are stored in a particular order. Methods
66
+ # such as #each process the headers in this order.
67
+ #
68
+ # When field names or values are added to the object they are
69
+ # frozen. This helps prevent accidental modification to what is
70
+ # stored in the object.
71
+ class Header
72
+ include Enumerable
73
+
74
+ class Field # :nodoc:
75
+ # fixme, document methadology for this (RFC2822)
76
+ EXTRACT_FIELD_NAME_RE = /\A([^\x00-\x1f\x7f-\xff :]+):\s*/o
77
+
78
+ class << self
79
+ def parse(field)
80
+ field = field.to_str
81
+ if field =~ EXTRACT_FIELD_NAME_RE
82
+ [ $1, $'.chomp ]
83
+ else
84
+ [ "", Field.value_strip(field) ]
85
+ end
86
+ end
87
+ end
88
+
89
+ def initialize(name, value = nil)
90
+ if value
91
+ @name = Field.name_strip(name.to_str).freeze
92
+ @value = Field.value_strip(value.to_str).freeze
93
+ @raw = nil
94
+ else
95
+ @raw = name.to_str.freeze
96
+ @name, @value = Field.parse(@raw)
97
+ @name.freeze
98
+ @value.freeze
99
+ end
100
+ end
101
+
102
+ attr_reader :name, :value, :raw
103
+
104
+ def ==(other)
105
+ other.kind_of?(self.class) &&
106
+ @name.downcase == other.name.downcase &&
107
+ @value == other.value
108
+ end
109
+
110
+ def Field.name_canonicalize(name)
111
+ name_strip(name.to_str).downcase
112
+ end
113
+
114
+ private
115
+
116
+ def Field.name_strip(name)
117
+ name.sub(/\s*:.*/, '')
118
+ end
119
+
120
+ def Field.value_strip(value)
121
+ if value.frozen?
122
+ value = value.dup
123
+ end
124
+ value.strip!
125
+ value
126
+ end
127
+
128
+ end
129
+
130
+ # Creates a new empty header object.
131
+ def initialize()
132
+ clear()
133
+ end
134
+
135
+ # Return the value of the first matching field of a field name, or
136
+ # nil if none found. If passed a Fixnum, returns the header
137
+ # indexed by the number.
138
+ def [](name_or_index)
139
+ if name_or_index.kind_of? Fixnum
140
+ temp = @fields[name_or_index]
141
+ temp = temp.value unless temp.nil?
142
+ else
143
+ name = Field.name_canonicalize(name_or_index)
144
+ result = detect { |n, v|
145
+ if n.downcase == name then true else false end
146
+ }
147
+ if result.nil? then nil else result[1] end
148
+ end
149
+ end
150
+
151
+ # Creates a copy of this header object. A new RMail::Header is
152
+ # created and the instance data is copied over. However, the new
153
+ # object will still reference the same strings held in the
154
+ # original object. Since these strings are frozen, this usually
155
+ # won't matter.
156
+ def dup
157
+ h = super
158
+ h.fields = @fields.dup
159
+ h.mbox_from = @mbox_from
160
+ h
161
+ end
162
+
163
+ # Creates a complete copy of this header object, including any
164
+ # singleton methods and strings. The returned object will be a
165
+ # complete and unrelated duplicate of the original.
166
+ def clone
167
+ h = super
168
+ h.fields = Marshal::load(Marshal::dump(@fields))
169
+ h.mbox_from = Marshal::load(Marshal::dump(@mbox_from))
170
+ h
171
+ end
172
+
173
+ # Delete all fields in this object. Returns self.
174
+ def clear()
175
+ @fields = []
176
+ @mbox_from = nil
177
+ self
178
+ end
179
+
180
+ # Replaces the contents of this header with that of another header
181
+ # object. Returns self.
182
+ def replace(other)
183
+ unless other.kind_of?(RMail::Header)
184
+ raise TypeError, "#{other.class.to_s} is not of type RMail::Header"
185
+ end
186
+ temp = other.dup
187
+ @fields = temp.fields
188
+ @mbox_from = temp.mbox_from
189
+ self
190
+ end
191
+
192
+ # Return the number of fields in this object.
193
+ def length
194
+ @fields.length
195
+ end
196
+ alias size length
197
+
198
+ # Return the value of the first matching field of a given name.
199
+ # If there is no such field, the value returned by the supplied
200
+ # block is returned. If no block is passed, the value of
201
+ # +default_value+ is returned. If no +default_value+ is
202
+ # specified, an IndexError exception is raised.
203
+ def fetch(name, *rest)
204
+ if rest.length > 1
205
+ raise ArgumentError, "wrong # of arguments(#{rest.length + 1} for 2)"
206
+ end
207
+ result = self[name]
208
+ if result.nil?
209
+ if block_given?
210
+ yield name
211
+ elsif rest.length == 1
212
+ rest[0]
213
+ else
214
+ raise IndexError, 'name not found'
215
+ end
216
+ else
217
+ result
218
+ end
219
+ end
220
+
221
+ # Returns the values of every field named +name+. If there are no
222
+ # such fields, the value returned by the block is returned. If no
223
+ # block is passed, the value of +default_value+ is returned. If
224
+ # no +default_value+ is specified, an IndexError exception is
225
+ # raised.
226
+ def fetch_all name, *rest
227
+ if rest.length > 1
228
+ raise ArgumentError, "wrong # of arguments(#{rest.length + 1} for 2)"
229
+ end
230
+ result = select(name)
231
+ if result.nil?
232
+ if block_given?
233
+ yield name
234
+ elsif rest.length == 1
235
+ rest[0]
236
+ else
237
+ raise IndexError, 'name not found'
238
+ end
239
+ else
240
+ result.collect { |n, v|
241
+ v
242
+ }
243
+ end
244
+ end
245
+
246
+ # Returns true if the message has a field named 'name'.
247
+ def field?(name)
248
+ ! self[name].nil?
249
+ end
250
+ alias member? field?
251
+ alias include? field?
252
+ alias has_key? field?
253
+ alias key? field?
254
+
255
+ # Deletes all fields with +name+. Returns self.
256
+ def delete(name)
257
+ name = Field.name_canonicalize(name.to_str)
258
+ delete_if { |n, v|
259
+ n.downcase == name
260
+ }
261
+ self
262
+ end
263
+
264
+ # Deletes the field at the specified index and returns its value.
265
+ def delete_at(index)
266
+ @fields[index, 1] = nil
267
+ self
268
+ end
269
+
270
+ # Deletes the field if the passed block returns true. Returns
271
+ # self.
272
+ def delete_if # yields: name, value
273
+ @fields.delete_if { |i|
274
+ yield i.name, i.value
275
+ }
276
+ self
277
+ end
278
+
279
+ # Executes block once for each field in the header, passing the
280
+ # key and value as parameters.
281
+ #
282
+ # Returns self.
283
+ def each # yields: name, value
284
+ @fields.each { |i|
285
+ yield(i.name, i.value)
286
+ }
287
+ end
288
+ alias each_pair each
289
+
290
+ # Executes block once for each field in the header, passing the
291
+ # field's name as a parameter.
292
+ #
293
+ # Returns self
294
+ def each_name
295
+ @fields.each { |i|
296
+ yield(i.name)
297
+ }
298
+ end
299
+ alias each_key each_name
300
+
301
+ # Executes block once for each field in the header, passing the
302
+ # field's value as a parameter.
303
+ #
304
+ # Returns self
305
+ def each_value
306
+ @fields.each { |i|
307
+ yield(i.value)
308
+ }
309
+ end
310
+
311
+ # Returns true if the header contains no fields
312
+ def empty?
313
+ @fields.empty?
314
+ end
315
+
316
+ # Returns an array of pairs [ name, value ] for all fields with
317
+ # one of the names passed.
318
+ def select(*names)
319
+ result = []
320
+ names.each { |name|
321
+ name = Field.name_canonicalize(name)
322
+ result.concat(find_all { |n, v|
323
+ n.downcase == name
324
+ })
325
+ }
326
+ result
327
+ end
328
+
329
+ # Returns an array consisting of the names of every field in this
330
+ # header.
331
+ def names
332
+ collect { |n, v|
333
+ n
334
+ }
335
+ end
336
+ alias keys names
337
+
338
+ # Add a new field with +name+ and +value+. When +index+ is nil
339
+ # (the default if not specified) the line is appended to the
340
+ # header, otherwise it is inserted at the specified index.
341
+ # E.g. an +index+ of 0 will prepend the header line.
342
+ #
343
+ # You can pass additional parameters for the header as a hash
344
+ # table +params+. Every key of the hash will be the name of the
345
+ # parameter, and every key's value the parameter value.
346
+ #
347
+ # E.g.
348
+ #
349
+ # header.add('Content-Type', 'multipart/mixed', nil,
350
+ # 'boundary' => 'the boundary')
351
+ #
352
+ # will add this header
353
+ #
354
+ # Content-Type: multipart/mixed; boundary="the boundary"
355
+ #
356
+ # Always returns self.
357
+ def add(name, value, index = nil, params = nil)
358
+ value = value.to_str
359
+ if params
360
+ value = value.dup
361
+ sep = "; "
362
+ params.each do |n, v|
363
+ value << sep
364
+ value << n.to_s
365
+ value << '='
366
+ v = v.to_s
367
+ if v =~ /^\w+$/
368
+ value << v
369
+ else
370
+ value << '"'
371
+ value << v
372
+ value << '"'
373
+ end
374
+ end
375
+ end
376
+ field = Field.new(name, value)
377
+ index ||= @fields.length
378
+ @fields[index, 0] = field
379
+ self
380
+ end
381
+
382
+ # Add a new field as a raw string together with a parsed
383
+ # name/value. This method is used mainly by the parser and
384
+ # regular programs should stick to #add.
385
+ def add_raw(raw)
386
+ @fields << Field.new(raw)
387
+ self
388
+ end
389
+
390
+ # First delete any fields with +name+, then append a new field
391
+ # with +name+, +value+, and +params+ as in #add.
392
+ def set(name, value, params = nil)
393
+ delete(name)
394
+ add(name, value, nil, params)
395
+ end
396
+
397
+ # Append a new field with +name+ and +value+. If you want control
398
+ # of where the field is inserted, see #add.
399
+ #
400
+ # Returns +value+.
401
+ def []=(name, value)
402
+ add(name, value)
403
+ value
404
+ end
405
+
406
+ # Returns true if the two objects have the same number of fields,
407
+ # in the same order, with the same values.
408
+ def ==(other)
409
+ return other.kind_of?(self.class) &&
410
+ @fields == other.fields &&
411
+ @mbox_from == other.mbox_from
412
+ end
413
+
414
+ # Returns a new array holding one [ name, value ] array per field
415
+ # in the header.
416
+ def to_a
417
+ @fields.collect { |field|
418
+ [ field.name, field.value ]
419
+ }
420
+ end
421
+
422
+ # Converts the header to a string, including any mbox from line.
423
+ # Equivalent to header.to_string(true).
424
+ def to_s
425
+ to_string(true)
426
+ end
427
+
428
+ # Converts the header to a string. If +mbox_from+ is true, then
429
+ # the mbox from line is also included.
430
+ def to_string(mbox_from = false)
431
+ s = ""
432
+ if mbox_from && ! @mbox_from.nil?
433
+ s << @mbox_from
434
+ s << "\n" unless @mbox_from[-1] == ?\n
435
+ end
436
+ @fields.each { |field|
437
+ if field.raw
438
+ s << field.raw
439
+ else
440
+ s << field.name
441
+ s << ': '
442
+ s << field.value
443
+ end
444
+ s << "\n" unless s[-1] == ?\n
445
+ }
446
+ s
447
+ end
448
+
449
+ # Determine if there is any fields that match the given +name+ and
450
+ # +value+.
451
+ #
452
+ # If +name+ is a String, all fields of that name are tested. If
453
+ # +name+ is a Regexp the field names are matched against the
454
+ # regexp (the field names are converted to lower case first). Use
455
+ # the regexp // if you want to test all field names.
456
+ #
457
+ # If +value+ is a String, it is converted to a case insensitive
458
+ # Regexp that matches the string. Otherwise, it must be a Regexp.
459
+ # Note that the field value may be folded across many lines, so
460
+ # you should use a multi-line Regexp. Also consider using a case
461
+ # insensitive Regexp. Use the regexp // if you want to match all
462
+ # possible field values.
463
+ #
464
+ # Returns true if there is a match, false otherwise.
465
+ #
466
+ # Example:
467
+ #
468
+ # if h.match?('x-ml-name', /ruby-dev/im)
469
+ # # do something
470
+ # end
471
+ #
472
+ # See also: #match
473
+ def match?(name, value)
474
+ massage_match_args(name, value) { |name, value|
475
+ match = detect {|n, v|
476
+ n =~ name && v =~ value
477
+ }
478
+ ! match.nil?
479
+ }
480
+ end
481
+
482
+ # Find all fields that match the given +name and +value+.
483
+ #
484
+ # If +name+ is a String, all fields of that name are tested. If
485
+ # +name+ is a Regexp, the field names are matched against the
486
+ # regexp (the field names are converted to lower case first). Use
487
+ # the regexp // if you want to test all field names.
488
+ #
489
+ # If +value+ is a String, it is converted to a case insensitive
490
+ # Regexp that matches the string. Otherwise, it must be a Regexp.
491
+ # Note that the field value may be folded across many lines, so
492
+ # you may need to use a multi-line Regexp. Also consider using a
493
+ # case insensitive Regexp. Use the regexp // if you want to match
494
+ # all possible field values.
495
+ #
496
+ # Returns a new RMail::Header holding all matching headers.
497
+ #
498
+ # Examples:
499
+ #
500
+ # received = header.match('Received', //)
501
+ # destinations = header.match(/^(to|cc|bcc)$/, //)
502
+ # bigfoot_received = header.match('received',
503
+ # /from.*by.*bigfoot\.com.*LiteMail/im)
504
+ #
505
+ # See also: #match?
506
+ def match(name, value)
507
+ massage_match_args(name, value) { |name, value|
508
+ header = RMail::Header.new
509
+ found = each { |n, v|
510
+ if n.downcase =~ name && value =~ v
511
+ header[n] = v
512
+ end
513
+ }
514
+ header
515
+ }
516
+ end
517
+
518
+ # Sets the "From " line commonly used in the Unix mbox mailbox
519
+ # format. The +value+ supplied should be the entire "From " line.
520
+ def mbox_from=(value)
521
+ @mbox_from = value
522
+ end
523
+
524
+ # Gets the "From " line previously set with mbox_from=, or nil.
525
+ def mbox_from
526
+ @mbox_from
527
+ end
528
+
529
+ # This returns the full content type of this message converted to
530
+ # lower case.
531
+ #
532
+ # If there is no content type header, returns the passed block is
533
+ # executed and its return value is returned. If no block is passed,
534
+ # the value of the +default+ argument is returned.
535
+ def content_type(default = nil)
536
+ if value = self['content-type']
537
+ value.strip.split(/\s*;\s*/)[0].downcase
538
+ else
539
+ if block_given?
540
+ yield
541
+ else
542
+ default
543
+ end
544
+ end
545
+ end
546
+
547
+ # This returns the main media type for this message converted to
548
+ # lower case. This is the first portion of the content type.
549
+ # E.g. a content type of <tt>text/plain</tt> has a media type of
550
+ # <tt>text</tt>.
551
+ #
552
+ # If there is no content type field, returns the passed block is
553
+ # executed and its return value is returned. If no block is
554
+ # passed, the value of the +default+ argument is returned.
555
+ def media_type(default = nil)
556
+ if value = content_type
557
+ value.split('/')[0]
558
+ else
559
+ if block_given?
560
+ yield
561
+ else
562
+ default
563
+ end
564
+ end
565
+ end
566
+
567
+ # This returns the media subtype for this message, converted to
568
+ # lower case. This is the second portion of the content type.
569
+ # E.g. a content type of <tt>text/plain</tt> has a media subtype
570
+ # of <tt>plain</tt>.
571
+ #
572
+ # If there is no content type field, returns the passed block is
573
+ # executed and its return value is returned. If no block is passed,
574
+ # the value of the +default+ argument is returned.
575
+ def subtype(default = nil)
576
+ if value = content_type
577
+ value.split('/')[1]
578
+ else
579
+ if block_given? then
580
+ yield
581
+ else
582
+ default
583
+ end
584
+ end
585
+ end
586
+
587
+ # This returns a hash of parameters. Each key in the hash is the
588
+ # name of the parameter in lower case and each value in the hash
589
+ # is the unquoted parameter value. If a parameter has no value,
590
+ # its value in the hash will be +true+.
591
+ #
592
+ # If the field or parameter does not exist or it is malformed in a
593
+ # way that makes it impossible to parse, then the passed block is
594
+ # executed and its return value is returned. If no block is
595
+ # passed, the value of the +default+ argument is returned.
596
+ def params(field_name, default = nil)
597
+ if params = params_quoted(field_name)
598
+ params.each { |name, value|
599
+ params[name] = value ? Utils.unquote(value) : nil
600
+ }
601
+ else
602
+ if block_given?
603
+ yield field_name
604
+ else
605
+ default
606
+ end
607
+ end
608
+ end
609
+
610
+ # This returns the parameter value for the given parameter in the
611
+ # given field. The value returned is unquoted.
612
+ #
613
+ # If the field or parameter does not exist or it is malformed in a
614
+ # way that makes it impossible to parse, then the passed block is
615
+ # executed and its return value is returned. If no block is
616
+ # passed, the value of the +default+ argument is returned.
617
+ def param(field_name, param_name, default = nil)
618
+ if field?(field_name)
619
+ params = params_quoted(field_name)
620
+ value = params[param_name]
621
+ return Utils.unquote(value) if value
622
+ end
623
+ if block_given?
624
+ yield field_name, param_name
625
+ else
626
+ default
627
+ end
628
+ end
629
+
630
+ # Set the boundary parameter of this message's Content-Type:
631
+ # field.
632
+ def set_boundary(boundary)
633
+ params = params_quoted('content-type')
634
+ params ||= {}
635
+ params['boundary'] = boundary
636
+ content_type = content_type()
637
+ content_type ||= "multipart/mixed"
638
+ delete('Content-Type')
639
+ add('Content-Type', content_type, nil, params)
640
+ end
641
+
642
+ # Return the value of the Date: field, parsed into a Time
643
+ # object. Returns nil if there is no Date: field or the field
644
+ # value could not be parsed.
645
+ def date
646
+ if value = self['date']
647
+ begin
648
+ # Rely on Ruby's standard time.rb to parse the time.
649
+ (Time.rfc2822(value) rescue Time.parse(value)).localtime
650
+ rescue
651
+ # Exceptions during time parsing just cause nil to be
652
+ # returned.
653
+ end
654
+ end
655
+ end
656
+
657
+ # Deletes any existing Date: fields and appends a new one
658
+ # corresponding to the given Time object.
659
+ def date=(time)
660
+ delete('Date')
661
+ add('Date', time.rfc2822)
662
+ end
663
+
664
+ # Returns the value of the From: header as an Array of
665
+ # RMail::Address objects.
666
+ #
667
+ # See #address_list_fetch for details on what is returned.
668
+ #
669
+ # This method does not return a single RMail::Address value
670
+ # because it is legal to have multiple addresses in a From:
671
+ # header.
672
+ #
673
+ # This method always returns at least the empty list. So if you
674
+ # are always only interested in the first from address (most
675
+ # likely the case), you can safely say:
676
+ #
677
+ # header.from.first
678
+ def from
679
+ address_list_fetch('from')
680
+ end
681
+
682
+ # Sets the From: field to the supplied address or addresses.
683
+ #
684
+ # See #address_list_assign for information on valid values for
685
+ # +addresses+.
686
+ #
687
+ # Note that the From: header usually contains only one address,
688
+ # but it is legal to have more than one.
689
+ def from=(addresses)
690
+ address_list_assign('From', addresses)
691
+ end
692
+
693
+ # Returns the value of the To: field as an Array of RMail::Address
694
+ # objects.
695
+ #
696
+ # See #address_list_fetch for details on what is returned.
697
+ def to
698
+ address_list_fetch('to')
699
+ end
700
+
701
+ # Sets the To: field to the supplied address or addresses.
702
+ #
703
+ # See #address_list_assign for information on valid values for
704
+ # +addresses+.
705
+ def to=(addresses)
706
+ address_list_assign('To', addresses)
707
+ end
708
+
709
+ # Returns the value of the Cc: field as an Array of RMail::Address
710
+ # objects.
711
+ #
712
+ # See #address_list_fetch for details on what is returned.
713
+ def cc
714
+ address_list_fetch('cc')
715
+ end
716
+
717
+ # Sets the Cc: field to the supplied address or addresses.
718
+ #
719
+ # See #address_list_assign for information on valid values for
720
+ # +addresses+.
721
+ def cc=(addresses)
722
+ address_list_assign('Cc', addresses)
723
+ end
724
+
725
+ # Returns the value of the Bcc: field as an Array of
726
+ # RMail::Address objects.
727
+ #
728
+ # See #address_list_fetch for details on what is returned.
729
+ def bcc
730
+ address_list_fetch('bcc')
731
+ end
732
+
733
+ # Sets the Bcc: field to the supplied address or addresses.
734
+ #
735
+ # See #address_list_assign for information on valid values for
736
+ # +addresses+.
737
+ def bcc=(addresses)
738
+ address_list_assign('Bcc', addresses)
739
+ end
740
+
741
+ # Returns the value of the Reply-To: header as an Array of
742
+ # RMail::Address objects.
743
+ def reply_to
744
+ address_list_fetch('reply-to')
745
+ end
746
+
747
+ # Sets the Reply-To: field to the supplied address or addresses.
748
+ #
749
+ # See #address_list_assign for information on valid values for
750
+ # +addresses+.
751
+ def reply_to=(addresses)
752
+ address_list_assign('Reply-To', addresses)
753
+ end
754
+
755
+ # Returns the value of this object's Message-Id: field.
756
+ def message_id
757
+ self['message-id']
758
+ end
759
+
760
+ # Sets the value of this object's Message-Id: field to a new
761
+ # random value.
762
+ #
763
+ # If you don't supply a +fqdn+ (fully qualified domain name) then
764
+ # one will be randomly generated for you. If a valid address
765
+ # exists in the From: field, its domain will be used as a basis.
766
+ #
767
+ # Part of the randomness in the header is taken from the header
768
+ # itself, so it is best to call this method after adding other
769
+ # fields to the header -- especially those that make it unique
770
+ # (Subject:, To:, Cc:, etc).
771
+ def add_message_id(fqdn = nil)
772
+
773
+ # If they don't supply a fqdn, we supply one for them.
774
+ #
775
+ # First grab the From: field and see if we can use a domain from
776
+ # there. If so, use that domain name plus the hash of the From:
777
+ # field's value (this guarantees that bob@example.com and
778
+ # sally@example.com will never have clashes).
779
+ #
780
+ # If there is no From: field, grab the current host name and use
781
+ # some randomness from Ruby's random number generator. Since
782
+ # Ruby's random number generator is fairly good this will
783
+ # suffice so long as it is seeded corretly.
784
+ #
785
+ # P.S. There is no portable way to get the fully qualified
786
+ # domain name of the current host. Those truly interested in
787
+ # generating "correct" message-ids should pass it in. We
788
+ # generate a hopefully random and unique domain name.
789
+ unless fqdn
790
+ unless fqdn = from.domains.first
791
+ require 'socket'
792
+ fqdn = sprintf("%s.invalid", Socket.gethostname)
793
+ end
794
+ else
795
+ raise ArgumentError, "fqdn must have at least one dot" unless
796
+ fqdn.index('.')
797
+ end
798
+
799
+ # Hash the header we have so far.
800
+ md5 = Digest::MD5.new
801
+ starting_digest = md5.digest
802
+ @fields.each { |f|
803
+ if f.raw
804
+ md5.update(f.raw)
805
+ else
806
+ md5.update(f.name) if f.name
807
+ md5.update(f.value) if f.value
808
+ end
809
+ }
810
+ if (digest = md5.digest) == starting_digest
811
+ digest = 0
812
+ end
813
+
814
+ set('Message-Id', sprintf("<%s.%s.%s.rubymail@%s>",
815
+ base36(Time.now.to_i),
816
+ base36(rand(MESSAGE_ID_MAXRAND)),
817
+ base36(digest),
818
+ fqdn))
819
+ end
820
+
821
+ # Return the subject of this message.
822
+ def subject
823
+ self['subject']
824
+ end
825
+
826
+ # Set the subject of this message
827
+ def subject=(string)
828
+ set('Subject', string)
829
+ end
830
+
831
+ # Returns an RMail::Address::List array holding all the recipients
832
+ # of this message. This uses the contents of the To, Cc, and Bcc
833
+ # fields. Duplicate addresses are eliminated.
834
+ def recipients
835
+ retval = RMail::Address::List.new
836
+ retval.concat(to)
837
+ retval.concat(cc)
838
+ retval.concat(bcc)
839
+ retval.uniq
840
+ end
841
+
842
+ # recipients
843
+
844
+ # Retrieve a given field's value as an RMail::Address::List of
845
+ # RMail::Address objects.
846
+ #
847
+ # This method is used to implement many of the convenience methods
848
+ # such as #from, #to, etc.
849
+ def address_list_fetch(field_name)
850
+ if values = fetch_all(field_name, nil)
851
+ list = nil
852
+ values.each { |value|
853
+ if list
854
+ list.concat(Address.parse(value))
855
+ else
856
+ list = Address.parse(value)
857
+ end
858
+ }
859
+ if list and !list.empty?
860
+ list
861
+ end
862
+ end or RMail::Address::List.new
863
+ end
864
+
865
+ # Set a given field to a list of supplied +addresses+.
866
+ #
867
+ # The +addresses+ may be a String, RMail::Address, or Array. If a
868
+ # String, it is parsed for valid email addresses and those found
869
+ # are used. If an RMail::Address, the result of
870
+ # RMail::Address#format is used. If an Array, each element of the
871
+ # array must be either a String or RMail::Address and is treated
872
+ # as above.
873
+ #
874
+ # This method is used to implement many of the convenience methods
875
+ # such as #from=, #to=, etc.
876
+ def address_list_assign(field_name, addresses)
877
+ if addresses.kind_of?(Array)
878
+ value = addresses.collect { |e|
879
+ if e.kind_of?(RMail::Address)
880
+ e.format
881
+ else
882
+ RMail::Address.parse(e.to_str).collect { |a|
883
+ a.format
884
+ }
885
+ end
886
+ }.flatten.join(", ")
887
+ set(field_name, value)
888
+ elsif addresses.kind_of?(RMail::Address)
889
+ set(field_name, addresses.format)
890
+ else
891
+ address_list_assign(field_name,
892
+ RMail::Address.parse(addresses.to_str))
893
+ end
894
+ end
895
+
896
+ protected
897
+
898
+ attr :fields, true
899
+
900
+ private
901
+
902
+ MESSAGE_ID_MAXRAND = 0x7fffffff
903
+
904
+ def string2num(string)
905
+ temp = 0
906
+ string.reverse.each_byte { |b|
907
+ temp <<= 8
908
+ temp |= b
909
+ }
910
+ return temp
911
+ end
912
+
913
+ BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz"
914
+ def base36(number)
915
+ if number.kind_of?(String)
916
+ number = string2num(number)
917
+ end
918
+ raise ArgumentError, "need non-negative number" if number < 0
919
+ return "0" if number == 0
920
+ result = ""
921
+ while number > 0
922
+ number, remainder = number.divmod(36)
923
+ result << BASE36[remainder]
924
+ end
925
+ return result.reverse!
926
+ end
927
+
928
+ PARAM_SCAN_RE = %r{
929
+ ;
930
+ |
931
+ [^;"]*"(?:|.*?(?:[^\\]|\\\\))"\s* # fix fontification "
932
+ |
933
+ [^;]+
934
+ }x
935
+
936
+ NAME_VALUE_SCAN_RE = %r{
937
+ =
938
+ |
939
+ [^="]*"(?:.*?(?:[^\\]|\\\\))" # fix fontification "\s*
940
+ |
941
+ [^=]+
942
+ }x
943
+
944
+ def params_quoted(field_name, default = nil)
945
+ if value = self[field_name]
946
+ params = {}
947
+ first = true
948
+ value.scan(PARAM_SCAN_RE) do |param|
949
+ if param != ';'
950
+ unless first
951
+ name, value = param.scan(NAME_VALUE_SCAN_RE).collect do |p|
952
+ if p == '=' then nil else p end
953
+ end.compact
954
+ if name && (name = name.strip.downcase) && name.length > 0
955
+ params[name] = (value || '').strip
956
+ end
957
+ else
958
+ first = false
959
+ end
960
+ end
961
+ end
962
+ params
963
+ else
964
+ if block_given? then yield field_name else default end
965
+ end
966
+ end
967
+
968
+ def massage_match_args(name, value)
969
+ case name
970
+ when String
971
+ name = /^#{Regexp.escape(Field.name_strip(name))}$/i
972
+ when Regexp
973
+ else
974
+ raise ArgumentError,
975
+ "name not a Regexp or String: #{name.class}:#{name.inspect}"
976
+ end
977
+ case value
978
+ when String
979
+ value = Regexp.new(Regexp.escape(value), Regexp::IGNORECASE)
980
+ when Regexp
981
+ else
982
+ raise ArgumentError, "value not a Regexp or String"
983
+ end
984
+ yield(name, value)
985
+ end
986
+ end
987
+ end