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.
- data/NEWS +309 -0
- data/NOTES +14 -0
- data/README +83 -0
- data/THANKS +25 -0
- data/TODO +112 -0
- data/guide/Intro.txt +122 -0
- data/guide/MIME.txt +6 -0
- data/guide/TableOfContents.txt +13 -0
- data/install.rb +1023 -0
- data/lib/rmail.rb +50 -0
- data/lib/rmail/address.rb +829 -0
- data/lib/rmail/header.rb +987 -0
- data/lib/rmail/mailbox.rb +62 -0
- data/lib/rmail/mailbox/mboxreader.rb +182 -0
- data/lib/rmail/message.rb +201 -0
- data/lib/rmail/parser.rb +412 -0
- data/lib/rmail/parser/multipart.rb +217 -0
- data/lib/rmail/parser/pushbackreader.rb +173 -0
- data/lib/rmail/serialize.rb +190 -0
- data/lib/rmail/utils.rb +59 -0
- data/rmail.gemspec +17 -0
- data/tests/addrgrammar.txt +113 -0
- data/tests/data/mbox.odd +4 -0
- data/tests/data/mbox.simple +8 -0
- data/tests/data/multipart/data.1 +5 -0
- data/tests/data/multipart/data.10 +1 -0
- data/tests/data/multipart/data.11 +9 -0
- data/tests/data/multipart/data.12 +9 -0
- data/tests/data/multipart/data.13 +3 -0
- data/tests/data/multipart/data.14 +3 -0
- data/tests/data/multipart/data.15 +3 -0
- data/tests/data/multipart/data.16 +3 -0
- data/tests/data/multipart/data.17 +0 -0
- data/tests/data/multipart/data.2 +5 -0
- data/tests/data/multipart/data.3 +2 -0
- data/tests/data/multipart/data.4 +3 -0
- data/tests/data/multipart/data.5 +1 -0
- data/tests/data/multipart/data.6 +2 -0
- data/tests/data/multipart/data.7 +3 -0
- data/tests/data/multipart/data.8 +5 -0
- data/tests/data/multipart/data.9 +4 -0
- data/tests/data/parser.badmime1 +4 -0
- data/tests/data/parser.badmime2 +6 -0
- data/tests/data/parser.nested-multipart +75 -0
- data/tests/data/parser.nested-simple +12 -0
- data/tests/data/parser.nested-simple2 +16 -0
- data/tests/data/parser.nested-simple3 +21 -0
- data/tests/data/parser.rfc822 +65 -0
- data/tests/data/parser.simple-mime +24 -0
- data/tests/data/parser/multipart.1 +8 -0
- data/tests/data/parser/multipart.10 +4 -0
- data/tests/data/parser/multipart.11 +12 -0
- data/tests/data/parser/multipart.12 +12 -0
- data/tests/data/parser/multipart.13 +6 -0
- data/tests/data/parser/multipart.14 +6 -0
- data/tests/data/parser/multipart.15 +6 -0
- data/tests/data/parser/multipart.16 +6 -0
- data/tests/data/parser/multipart.2 +8 -0
- data/tests/data/parser/multipart.3 +5 -0
- data/tests/data/parser/multipart.4 +6 -0
- data/tests/data/parser/multipart.5 +4 -0
- data/tests/data/parser/multipart.6 +5 -0
- data/tests/data/parser/multipart.7 +6 -0
- data/tests/data/parser/multipart.8 +8 -0
- data/tests/data/parser/multipart.9 +7 -0
- data/tests/data/transparency/absolute.1 +5 -0
- data/tests/data/transparency/absolute.2 +1 -0
- data/tests/data/transparency/absolute.3 +2 -0
- data/tests/data/transparency/absolute.4 +3 -0
- data/tests/data/transparency/absolute.5 +4 -0
- data/tests/data/transparency/absolute.6 +49 -0
- data/tests/data/transparency/message.1 +73 -0
- data/tests/data/transparency/message.2 +34 -0
- data/tests/data/transparency/message.3 +63 -0
- data/tests/data/transparency/message.4 +5 -0
- data/tests/data/transparency/message.5 +15 -0
- data/tests/data/transparency/message.6 +1185 -0
- data/tests/runtests.rb +35 -0
- data/tests/testaddress.rb +1192 -0
- data/tests/testbase.rb +207 -0
- data/tests/testheader.rb +1207 -0
- data/tests/testmailbox.rb +47 -0
- data/tests/testmboxreader.rb +161 -0
- data/tests/testmessage.rb +257 -0
- data/tests/testparser.rb +634 -0
- data/tests/testparsermultipart.rb +205 -0
- data/tests/testpushbackreader.rb +40 -0
- data/tests/testserialize.rb +264 -0
- data/tests/testtestbase.rb +112 -0
- data/tests/testtranspparency.rb +105 -0
- metadata +143 -0
data/lib/rmail/header.rb
ADDED
@@ -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
|