require 'mail/fields'
# encoding: utf-8
module Mail
# Provides a single class to call to create a new structured or unstructured
# field. Works out per RFC what field of field it is being given and returns
# the correct field of class back on new.
#
# ===Per RFC 2822
#
# 2.2. Header Fields
#
# Header fields are lines composed of a field name, followed by a colon
# (":"), followed by a field body, and terminated by CRLF. A field
# name MUST be composed of printable US-ASCII characters (i.e.,
# characters that have values between 33 and 126, inclusive), except
# colon. A field body may be composed of any US-ASCII characters,
# except for CR and LF. However, a field body may contain CRLF when
# used in header "folding" and "unfolding" as described in section
# 2.2.3. All field bodies MUST conform to the syntax described in
# sections 3 and 4 of this standard.
#
class Field
include Utilities
include Comparable
STRUCTURED_FIELDS = %w[ bcc cc content-description content-disposition
content-id content-location content-transfer-encoding
content-type date from in-reply-to keywords message-id
mime-version received references reply-to
resent-bcc resent-cc resent-date resent-from
resent-message-id resent-sender resent-to
return-path sender to ]
KNOWN_FIELDS = STRUCTURED_FIELDS + ['comments', 'subject']
FIELDS_MAP = {
"to" => ToField,
"cc" => CcField,
"bcc" => BccField,
"message-id" => MessageIdField,
"in-reply-to" => InReplyToField,
"references" => ReferencesField,
"subject" => SubjectField,
"comments" => CommentsField,
"keywords" => KeywordsField,
"date" => DateField,
"from" => FromField,
"sender" => SenderField,
"reply-to" => ReplyToField,
"resent-date" => ResentDateField,
"resent-from" => ResentFromField,
"resent-sender" => ResentSenderField,
"resent-to" => ResentToField,
"resent-cc" => ResentCcField,
"resent-bcc" => ResentBccField,
"resent-message-id" => ResentMessageIdField,
"return-path" => ReturnPathField,
"received" => ReceivedField,
"mime-version" => MimeVersionField,
"content-transfer-encoding" => ContentTransferEncodingField,
"content-description" => ContentDescriptionField,
"content-disposition" => ContentDispositionField,
"content-type" => ContentTypeField,
"content-id" => ContentIdField,
"content-location" => ContentLocationField,
}
FIELD_NAME_MAP = FIELDS_MAP.inject({}) do |map, (field, field_klass)|
map.update(field => field_klass::CAPITALIZED_FIELD)
end
# Generic Field Exception
class FieldError < StandardError
end
# Raised when a parsing error has occurred (ie, a StructuredField has tried
# to parse a field that is invalid or improperly written)
class ParseError < FieldError #:nodoc:
attr_accessor :element, :value, :reason
def initialize(element, value, reason)
@element = element
@value = value
@reason = reason
super("#{element} can not parse |#{value}|\nReason was: #{reason}")
end
end
# Raised when attempting to set a structured field's contents to an invalid syntax
class SyntaxError < FieldError #:nodoc:
end
# Accepts a string:
#
# Field.new("field-name: field data")
#
# Or name, value pair:
#
# Field.new("field-name", "value")
#
# Or a name by itself:
#
# Field.new("field-name")
#
# Note, does not want a terminating carriage return. Returns
# self appropriately parsed. If value is not a string, then
# it will be passed through as is, for example, content-type
# field can accept an array with the type and a hash of
# parameters:
#
# Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
def initialize(name, value = nil, charset = 'utf-8')
case
when name.index(COLON) # Field.new("field-name: field data")
@charset = value.blank? ? charset : value
@name = name[FIELD_PREFIX]
@raw_value = name
@value = nil
when value.blank? # Field.new("field-name")
@name = name
@value = nil
@raw_value = nil
@charset = charset
else # Field.new("field-name", "value")
@name = name
@value = value
@raw_value = nil
@charset = charset
end
@name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
end
def field=(value)
@field = value
end
def field
_, @value = split(@raw_value) if @raw_value && !@value
@field ||= create_field(@name, @value, @charset)
end
def name
@name
end
def value
field.value
end
def value=(val)
@field = create_field(name, val, @charset)
end
def to_s
field.to_s
end
def inspect
"#<#{self.class.name} 0x#{(object_id * 2).to_s(16)} #{instance_variables.map do |ivar|
"#{ivar}=#{instance_variable_get(ivar).inspect}"
end.join(" ")}>"
end
def update(name, value)
@field = create_field(name, value, @charset)
end
def same( other )
match_to_s(other.name, self.name)
end
def responsible_for?( val )
name.to_s.casecmp(val.to_s) == 0
end
alias_method :==, :same
def <=>( other )
self.field_order_id <=> other.field_order_id
end
def field_order_id
@field_order_id ||= (FIELD_ORDER_LOOKUP[self.name.to_s.downcase] || 100)
end
def method_missing(name, *args, &block)
field.send(name, *args, &block)
end
FIELD_ORDER = %w[ return-path received
resent-date resent-from resent-sender resent-to
resent-cc resent-bcc resent-message-id
date from sender reply-to to cc bcc
message-id in-reply-to references
subject comments keywords
mime-version content-type content-transfer-encoding
content-location content-disposition content-description ]
FIELD_ORDER_LOOKUP = Hash[FIELD_ORDER.each_with_index.to_a]
private
def split(raw_field)
match_data = raw_field.mb_chars.match(FIELD_SPLIT)
[match_data[1].to_s.mb_chars.strip, match_data[2].to_s.mb_chars.strip.to_s]
rescue
STDERR.puts "WARNING: Could not parse (and so ignoring) '#{raw_field}'"
end
# 2.2.3. Long Header Fields
#
# The process of moving from this folded multiple-line representation
# of a header field to its single line representation is called
# "unfolding". Unfolding is accomplished by simply removing any CRLF
# that is immediately followed by WSP. Each header field should be
# treated in its unfolded form for further syntactic and semantic
# evaluation.
def unfold(string)
string.gsub(/[\r\n \t]+/m, ' ')
end
def create_field(name, value, charset)
value = unfold(value) if value.is_a?(String)
begin
new_field(name, value, charset)
rescue Mail::Field::ParseError => e
field = Mail::UnstructuredField.new(name, value)
field.errors << [name, value, e]
field
end
end
def new_field(name, value, charset)
lower_case_name = name.to_s.downcase
if field_klass = FIELDS_MAP[lower_case_name]
field_klass.new(value, charset)
else
OptionalField.new(name, value, charset)
end
end
end
end