class RMail::Header
Overview¶ ↑
The RMail::Header
class supports the creation and manipulation of RFC2822 mail headers.
A mail header is a little bit like a Hash. The fields are keyed by a string field name. It is also a little bit like an Array, since the fields are in a specific order. This class provides many of the methods of both the Hash and Array class. It also includes the Enumerable module.
Terminology¶ ↑
- header
-
The entire header. Each
RMail::Header
object is one mail header. - field
-
An element of the header. Fields have a name and a value. For example, the field “Subject: Hi Mom!” has a name of “Subject” and a value of “Hi Mom!”
- name
-
A name of a field. For example: “Subject” or “From”.
- value
-
The value of a field.
Conventions¶ ↑
The header’s fields are stored in a particular order. Methods such as each
process the headers in this order.
When field names or values are added to the object they are frozen. This helps prevent accidental modification to what is stored in the object.
Constants
- BASE36
- MESSAGE_ID_MAXRAND
- NAME_VALUE_SCAN_RE
- PARAM_SCAN_RE
Attributes
Public Class Methods
Creates a new empty header object.
# File lib/rmail/header.rb, line 134 def initialize() clear() end
Public Instance Methods
Returns true if the two objects have the same number of fields, in the same order, with the same values.
# File lib/rmail/header.rb, line 411 def ==(other) return other.kind_of?(self.class) && @fields == other.fields && @mbox_from == other.mbox_from end
Return the value of the first matching field of a field name, or nil if none found. If passed an Integer, returns the header indexed by the number.
# File lib/rmail/header.rb, line 141 def [](name_or_index) if name_or_index.kind_of? Integer temp = @fields[name_or_index] temp = temp.value unless temp.nil? else name = Field.name_canonicalize(name_or_index) result = detect { |n, v| if n.downcase == name then true else false end } if result.nil? then nil else result[1] end end end
Append a new field with name
and value
. If you want control of where the field is inserted, see add
.
Returns value
.
# File lib/rmail/header.rb, line 404 def []=(name, value) add(name, value) value end
Add a new field with name
and value
. When index
is nil (the default if not specified) the line is appended to the header, otherwise it is inserted at the specified index. E.g. an index
of 0 will prepend the header line.
You can pass additional parameters for the header as a hash table params
. Every key of the hash will be the name of the parameter, and every key’s value the parameter value.
E.g.
header.add('Content-Type', 'multipart/mixed', nil, 'boundary' => 'the boundary')
will add this header
Content-Type: multipart/mixed; boundary="the boundary"
Always returns self.
# File lib/rmail/header.rb, line 360 def add(name, value, index = nil, params = nil) value = value.to_str if params value = value.dup sep = "; " params.each do |n, v| value << sep value << n.to_s value << '=' v = v.to_s if v =~ /^\w+$/ value << v else value << '"' value << v value << '"' end end end field = Field.new(name, value) index ||= @fields.length @fields[index, 0] = field self end
Sets the value of this object’s Message-Id: field to a new random value.
If you don’t supply a fqdn
(fully qualified domain name) then one will be randomly generated for you. If a valid address exists in the From: field, its domain will be used as a basis.
Part of the randomness in the header is taken from the header itself, so it is best to call this method after adding other fields to the header – especially those that make it unique (Subject:, To:, Cc:, etc).
# File lib/rmail/header.rb, line 775 def add_message_id(fqdn = nil) # If they don't supply a fqdn, we supply one for them. # # First grab the From: field and see if we can use a domain from # there. If so, use that domain name plus the hash of the From: # field's value (this guarantees that bob@example.com and # sally@example.com will never have clashes). # # If there is no From: field, grab the current host name and use # some randomness from Ruby's random number generator. Since # Ruby's random number generator is fairly good this will # suffice so long as it is seeded corretly. # # P.S. There is no portable way to get the fully qualified # domain name of the current host. Those truly interested in # generating "correct" message-ids should pass it in. We # generate a hopefully random and unique domain name. unless fqdn unless fqdn = from.domains.first require 'socket' fqdn = sprintf("%s.invalid", Socket.gethostname) end else raise ArgumentError, "fqdn must have at least one dot" unless fqdn.index('.') end # Hash the header we have so far. md5 = Digest::MD5.new starting_digest = md5.digest @fields.each { |f| if f.raw md5.update(f.raw) else md5.update(f.name) if f.name md5.update(f.value) if f.value end } if (digest = md5.digest) == starting_digest digest = 0 end set('Message-Id', sprintf("<%s.%s.%s.rubymail@%s>", base36(Time.now.to_i), base36(rand(MESSAGE_ID_MAXRAND)), base36(digest), fqdn)) end
Add a new field as a raw string together with a parsed name/value. This method is used mainly by the parser and regular programs should stick to add
.
# File lib/rmail/header.rb, line 388 def add_raw(raw) @fields << Field.new(raw) self end
Set a given field to a list of supplied addresses
.
The addresses
may be a String, RMail::Address
, or Array. If a String, it is parsed for valid email addresses and those found are used. If an RMail::Address
, the result of RMail::Address#format
is used. If an Array, each element of the array must be either a String or RMail::Address
and is treated as above.
This method is used to implement many of the convenience methods such as from=
, to=
, etc.
# File lib/rmail/header.rb, line 874 def address_list_assign(field_name, addresses) if addresses.kind_of?(Array) value = addresses.collect { |e| if e.kind_of?(RMail::Address) e.format else RMail::Address.parse(e.to_str).collect { |a| a.format } end }.flatten.join(", ") set(field_name, value) elsif addresses.kind_of?(RMail::Address) set(field_name, addresses.format) else address_list_assign(field_name, RMail::Address.parse(addresses.to_str)) end end
Retrieve a given field’s value as an RMail::Address::List
of RMail::Address
objects.
This method is used to implement many of the convenience methods such as from
, to
, etc.
# File lib/rmail/header.rb, line 847 def address_list_fetch(field_name) if values = fetch_all(field_name, nil) list = nil values.each { |value| if list list.concat(Address.parse(value)) else list = Address.parse(value) end } if list and !list.empty? list end end or RMail::Address::List.new end
Returns the value of the Bcc: field as an Array of RMail::Address
objects.
See address_list_fetch
for details on what is returned.
# File lib/rmail/header.rb, line 733 def bcc address_list_fetch('bcc') end
Sets the Bcc: field to the supplied address or addresses.
See address_list_assign
for information on valid values for addresses
.
# File lib/rmail/header.rb, line 741 def bcc=(addresses) address_list_assign('Bcc', addresses) end
Returns the value of the Cc: field as an Array of RMail::Address
objects.
See address_list_fetch
for details on what is returned.
# File lib/rmail/header.rb, line 717 def cc address_list_fetch('cc') end
Sets the Cc: field to the supplied address or addresses.
See address_list_assign
for information on valid values for addresses
.
# File lib/rmail/header.rb, line 725 def cc=(addresses) address_list_assign('Cc', addresses) end
Delete all fields in this object. Returns self.
# File lib/rmail/header.rb, line 177 def clear() @fields = [] @mbox_from = nil self end
Creates a complete copy of this header object, including any singleton methods and strings. The returned object will be a complete and unrelated duplicate of the original.
# File lib/rmail/header.rb, line 169 def clone h = super h.fields = Marshal::load(Marshal::dump(@fields)) h.mbox_from = Marshal::load(Marshal::dump(@mbox_from)) h end
This returns the full content type of this message converted to lower case.
If there is no content type header, returns the passed block is executed and its return value is returned. If no block is passed, the value of the default
argument is returned.
# File lib/rmail/header.rb, line 538 def content_type(default = nil) content_type = self['content-type'] if content_type && content_type.length > 0 content_type.strip.split(/\s*;\s*/)[0].downcase else if block_given? yield else default end end end
Return the value of the Date: field, parsed into a Time object. Returns nil if there is no Date: field or the field value could not be parsed.
# File lib/rmail/header.rb, line 649 def date if value = self['date'] begin # Rely on Ruby's standard time.rb to parse the time. (Time.rfc2822(value) rescue Time.parse(value)).localtime rescue # Exceptions during time parsing just cause nil to be # returned. end end end
Deletes any existing Date: fields and appends a new one corresponding to the given Time object.
# File lib/rmail/header.rb, line 663 def date=(time) delete('Date') add('Date', time.rfc2822) end
Deletes all fields with name
. Returns self.
# File lib/rmail/header.rb, line 259 def delete(name) name = Field.name_canonicalize(name.to_str) delete_if { |n, v| n.downcase == name } self end
Deletes the field at the specified index and returns its value.
# File lib/rmail/header.rb, line 268 def delete_at(index) @fields.delete_at(index) self end
Deletes the field if the passed block returns true. Returns self.
# File lib/rmail/header.rb, line 275 def delete_if # yields: name, value @fields.delete_if { |i| yield i.name, i.value } self end
Creates a copy of this header object. A new RMail::Header
is created and the instance data is copied over. However, the new object will still reference the same strings held in the original object. Since these strings are frozen, this usually won’t matter.
# File lib/rmail/header.rb, line 159 def dup h = super h.fields = @fields.dup h.mbox_from = @mbox_from h end
Executes block once for each field in the header, passing the key and value as parameters.
Returns self.
# File lib/rmail/header.rb, line 286 def each # yields: name, value @fields.each { |i| yield [i.name, i.value] } end
Executes block once for each field in the header, passing the field’s name as a parameter.
Returns self
# File lib/rmail/header.rb, line 297 def each_name @fields.each { |i| yield(i.name) } end
Executes block once for each field in the header, passing the field’s value as a parameter.
Returns self
# File lib/rmail/header.rb, line 308 def each_value @fields.each { |i| yield(i.value) } end
Returns true if the header contains no fields
# File lib/rmail/header.rb, line 315 def empty? @fields.empty? end
Return the value of the first matching field of a given name. If there is no such field, the value returned by the supplied block is returned. If no block is passed, the value of default_value
is returned. If no default_value
is specified, an IndexError exception is raised.
# File lib/rmail/header.rb, line 206 def fetch(name, *rest) if rest.length > 1 raise ArgumentError, "wrong # of arguments(#{rest.length + 1} for 2)" end result = self[name] if result.nil? if block_given? yield name elsif rest.length == 1 rest[0] else raise IndexError, 'name not found' end else result end end
Returns the values of every field named name
. If there are no such fields, the value returned by the block is returned. If no block is passed, the value of default_value
is returned. If no default_value
is specified, an IndexError exception is raised.
# File lib/rmail/header.rb, line 229 def fetch_all name, *rest if rest.length > 1 raise ArgumentError, "wrong # of arguments(#{rest.length + 1} for 2)" end result = select(name) if result.nil? if block_given? yield name elsif rest.length == 1 rest[0] else raise IndexError, 'name not found' end else result.collect { |n, v| v } end end
Returns true if the message has a field named ‘name’.
# File lib/rmail/header.rb, line 250 def field?(name) ! self[name].nil? end
Returns the value of the From: header as an Array of RMail::Address
objects.
See address_list_fetch
for details on what is returned.
This method does not return a single RMail::Address
value because it is legal to have multiple addresses in a From: header.
This method always returns at least the empty list. So if you are always only interested in the first from address (most likely the case), you can safely say:
header.from.first
# File lib/rmail/header.rb, line 682 def from address_list_fetch('from') end
Sets the From: field to the supplied address or addresses.
See address_list_assign
for information on valid values for addresses
.
Note that the From: header usually contains only one address, but it is legal to have more than one.
# File lib/rmail/header.rb, line 693 def from=(addresses) address_list_assign('From', addresses) end
Return the number of fields in this object.
# File lib/rmail/header.rb, line 196 def length @fields.length end
Find all fields that match the given +name and value
.
If name
is a String, all fields of that name are tested. If name
is a Regexp, the field names are matched against the regexp (the field names are converted to lower case first). Use the regexp // if you want to test all field names.
If value
is a String, it is converted to a case insensitive Regexp that matches the string. Otherwise, it must be a Regexp. Note that the field value may be folded across many lines, so you may need to use a multi-line Regexp. Also consider using a case insensitive Regexp. Use the regexp // if you want to match all possible field values.
Returns a new RMail::Header
holding all matching headers.
Examples:
received = header.match('Received', //) destinations = header.match(/^(to|cc|bcc)$/, //) bigfoot_received = header.match('received', /from.*by.*bigfoot\.com.*LiteMail/im)
See also: match?
# File lib/rmail/header.rb, line 509 def match(name, value) massage_match_args(name, value) { |mname, mvalue| header = RMail::Header.new each { |n, v| if n.downcase =~ mname && mvalue =~ v header[n] = v end } header } end
Determine if there is any fields that match the given name
and value
.
If name
is a String, all fields of that name are tested. If name
is a Regexp the field names are matched against the regexp (the field names are converted to lower case first). Use the regexp // if you want to test all field names.
If value
is a String, it is converted to a case insensitive Regexp that matches the string. Otherwise, it must be a Regexp. Note that the field value may be folded across many lines, so you should use a multi-line Regexp. Also consider using a case insensitive Regexp. Use the regexp // if you want to match all possible field values.
Returns true if there is a match, false otherwise.
Example:
if h.match?('x-ml-name', /ruby-dev/im) # do something end
See also: match
# File lib/rmail/header.rb, line 476 def match?(name, value) massage_match_args(name, value) { |mname, mvalue| match = detect {|n, v| n =~ mname && v =~ mvalue } ! match.nil? } end
Gets the “From ” line previously set with mbox_from
=, or nil.
# File lib/rmail/header.rb, line 528 def mbox_from @mbox_from end
Sets the “From ” line commonly used in the Unix mbox mailbox format. The value
supplied should be the entire “From ” line.
# File lib/rmail/header.rb, line 523 def mbox_from=(value) @mbox_from = value end
This returns the main media type for this message converted to lower case. This is the first portion of the content type. E.g. a content type of text/plain
has a media type of text
.
If there is no content type field, returns the passed block is executed and its return value is returned. If no block is passed, the value of the default
argument is returned.
# File lib/rmail/header.rb, line 559 def media_type(default = nil) if value = content_type value.split('/')[0] else if block_given? yield else default end end end
Returns the value of this object’s Message-Id: field.
# File lib/rmail/header.rb, line 760 def message_id self['message-id'] end
Returns an array consisting of the names of every field in this header.
# File lib/rmail/header.rb, line 334 def names collect { |n, v| n } end
This returns the parameter value for the given parameter in the given field. The value returned is unquoted.
If the field or parameter does not exist or it is malformed in a way that makes it impossible to parse, then the passed block is executed and its return value is returned. If no block is passed, the value of the default
argument is returned.
# File lib/rmail/header.rb, line 621 def param(field_name, param_name, default = nil) if field?(field_name) params = params_quoted(field_name) value = params[param_name] return Utils.unquote(value) if value end if block_given? yield field_name, param_name else default end end
This returns a hash of parameters. Each key in the hash is the name of the parameter in lower case and each value in the hash is the unquoted parameter value. If a parameter has no value, its value in the hash will be true
.
If the field or parameter does not exist or it is malformed in a way that makes it impossible to parse, then the passed block is executed and its return value is returned. If no block is passed, the value of the default
argument is returned.
# File lib/rmail/header.rb, line 600 def params(field_name, default = nil) if params = params_quoted(field_name) params.each { |name, value| params[name] = value ? Utils.unquote(value) : nil } else if block_given? yield field_name else default end end end
Returns an RMail::Address::List
array holding all the recipients of this message. This uses the contents of the To, Cc, and Bcc fields. Duplicate addresses are eliminated.
# File lib/rmail/header.rb, line 838 def recipients RMail::Address::List.new([ to, cc, bcc ].flatten.uniq) end
Replaces the contents of this header with that of another header object. Returns self.
# File lib/rmail/header.rb, line 185 def replace(other) unless other.kind_of?(RMail::Header) raise TypeError, "#{other.class.to_s} is not of type RMail::Header" end temp = other.dup @fields = temp.fields @mbox_from = temp.mbox_from self end
Returns the value of the Reply-To: header as an Array of RMail::Address
objects.
# File lib/rmail/header.rb, line 747 def reply_to address_list_fetch('reply-to') end
Sets the Reply-To: field to the supplied address or addresses.
See address_list_assign
for information on valid values for addresses
.
# File lib/rmail/header.rb, line 755 def reply_to=(addresses) address_list_assign('Reply-To', addresses) end
Returns an array of pairs [ name, value ] for all fields with one of the names passed.
# File lib/rmail/header.rb, line 321 def select(*names) result = [] names.each { |name| name = Field.name_canonicalize(name) result.concat(find_all { |n, v| n.downcase == name }) } result end
First delete any fields with name
, then append a new field with name
, value
, and params
as in add
.
# File lib/rmail/header.rb, line 395 def set(name, value, params = nil) delete(name) add(name, value, nil, params) end
Set the boundary parameter of this message’s Content-Type: field.
# File lib/rmail/header.rb, line 636 def set_boundary(boundary) params = params('content-type') params ||= {} params['boundary'] = boundary content_type = content_type() content_type ||= "multipart/mixed" delete('Content-Type') add('Content-Type', content_type, nil, params) end
Return the subject of this message.
# File lib/rmail/header.rb, line 826 def subject self['subject'] end
Set the subject of this message
# File lib/rmail/header.rb, line 831 def subject=(string) set('Subject', string) end
This returns the media subtype for this message, converted to lower case. This is the second portion of the content type. E.g. a content type of text/plain
has a media subtype of plain
.
If there is no content type field, returns the passed block is executed and its return value is returned. If no block is passed, the value of the default
argument is returned.
# File lib/rmail/header.rb, line 579 def subtype(default = nil) if value = content_type value.split('/')[1] else if block_given? then yield else default end end end
Returns the value of the To: field as an Array of RMail::Address
objects.
See address_list_fetch
for details on what is returned.
# File lib/rmail/header.rb, line 701 def to address_list_fetch('to') end
Sets the To: field to the supplied address or addresses.
See address_list_assign
for information on valid values for addresses
.
# File lib/rmail/header.rb, line 709 def to=(addresses) address_list_assign('To', addresses) end
Returns a new array holding one [ name, value ] array per field in the header.
# File lib/rmail/header.rb, line 419 def to_a @fields.collect { |field| [ field.name, field.value ] } end
Converts the header to a string, including any mbox from line. Equivalent to header.to_string(true).
# File lib/rmail/header.rb, line 427 def to_s to_string(true) end
Converts the header to a string. If mbox_from
is true, then the mbox from line is also included.
# File lib/rmail/header.rb, line 433 def to_string(mbox_from = false) s = "" if mbox_from && ! @mbox_from.nil? s << @mbox_from s << "\n" unless @mbox_from[-1] == ?\n end @fields.each { |field| if field.raw s << field.raw else s << field.name s << ': ' s << field.value end s << "\n" unless s[-1] == ?\n } s end
Private Instance Methods
# File lib/rmail/header.rb, line 912 def base36(number) if number.kind_of?(String) number = string2num(number) end raise ArgumentError, "need non-negative number" if number < 0 return "0" if number == 0 result = "" while number > 0 number, remainder = number.divmod(36) result << BASE36[remainder] end return result.reverse! end
# File lib/rmail/header.rb, line 966 def massage_match_args(name, value) case name when String name = /^#{Regexp.escape(Field.name_strip(name))}$/i when Regexp else raise ArgumentError, "name not a Regexp or String: #{name.class}:#{name.inspect}" end case value when String value = Regexp.new(Regexp.escape(value), Regexp::IGNORECASE) when Regexp else raise ArgumentError, "value not a Regexp or String" end yield(name, value) end
# File lib/rmail/header.rb, line 942 def params_quoted(field_name, default = nil) if value = self[field_name] params = {} first = true value.scan(PARAM_SCAN_RE) do |param| if param != ';' unless first name, value = param.scan(NAME_VALUE_SCAN_RE).collect do |p| if p == '=' then nil else p end end.compact if name && (name = name.strip.downcase) && name.length > 0 params[name] = (value || '').strip end else first = false end end end params else if block_given? then yield field_name else default end end end def massage_match_args(name, value) case name when String name = /^#{Regexp.escape(Field.name_strip(name))}$/i when Regexp else raise ArgumentError, "name not a Regexp or String: #{name.class}:#{name.inspect}" end case value when String value = Regexp.new(Regexp.escape(value), Regexp::IGNORECASE) when Regexp else raise ArgumentError, "value not a Regexp or String" end yield(name, value) end end
# File lib/rmail/header.rb, line 902 def string2num(string) temp = 0 string.reverse.each_byte { |b| temp <<= 8 temp |= b } return temp end