rmail 0.17

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