mail/lib/mail/message.rb

2153 lines
66 KiB
Ruby

# encoding: utf-8
# frozen_string_literal: true
require 'mail/constants'
require 'mail/utilities'
require 'yaml'
module Mail
# The Message class provides a single point of access to all things to do with an
# email message.
#
# You create a new email message by calling the Mail::Message.new method, or just
# Mail.new
#
# A Message object by default has the following objects inside it:
#
# * A Header object which contains all information and settings of the header of the email
# * Body object which contains all parts of the email that are not part of the header, this
# includes any attachments, body text, MIME parts etc.
#
# ==Per RFC2822
#
# 2.1. General Description
#
# At the most basic level, a message is a series of characters. A
# message that is conformant with this standard is comprised of
# characters with values in the range 1 through 127 and interpreted as
# US-ASCII characters [ASCII]. For brevity, this document sometimes
# refers to this range of characters as simply "US-ASCII characters".
#
# Note: This standard specifies that messages are made up of characters
# in the US-ASCII range of 1 through 127. There are other documents,
# specifically the MIME document series [RFC2045, RFC2046, RFC2047,
# RFC2048, RFC2049], that extend this standard to allow for values
# outside of that range. Discussion of those mechanisms is not within
# the scope of this standard.
#
# Messages are divided into lines of characters. A line is a series of
# characters that is delimited with the two characters carriage-return
# and line-feed; that is, the carriage return (CR) character (ASCII
# value 13) followed immediately by the line feed (LF) character (ASCII
# value 10). (The carriage-return/line-feed pair is usually written in
# this document as "CRLF".)
#
# A message consists of header fields (collectively called "the header
# of the message") followed, optionally, by a body. The header is a
# sequence of lines of characters with special syntax as defined in
# this standard. The body is simply a sequence of characters that
# follows the header and is separated from the header by an empty line
# (i.e., a line with nothing preceding the CRLF).
class Message
# ==Making an email
#
# You can make an new mail object via a block, passing a string, file or direct assignment.
#
# ===Making an email via a block
#
# mail = Mail.new do |m|
# m.from 'mikel@test.lindsaar.net'
# m.to 'you@test.lindsaar.net'
# m.subject 'This is a test email'
# m.body File.read('body.txt')
# end
#
# mail.to_s #=> "From: mikel@test.lindsaar.net\r\nTo: you@...
#
# If may also pass a block with no arguments, in which case it will
# be evaluated in the scope of the new message instance:
#
# mail = Mail.new do
# from 'mikel@test.lindsaar.net'
# # …
# end
#
# ===Making an email via passing a string
#
# mail = Mail.new("To: mikel@test.lindsaar.net\r\nSubject: Hello\r\n\r\nHi there!")
# mail.body.to_s #=> 'Hi there!'
# mail.subject #=> 'Hello'
# mail.to #=> 'mikel@test.lindsaar.net'
#
# ===Making an email from a file
#
# mail = Mail.read('path/to/file.eml')
# mail.body.to_s #=> 'Hi there!'
# mail.subject #=> 'Hello'
# mail.to #=> 'mikel@test.lindsaar.net'
#
# ===Making an email via assignment
#
# You can assign values to a mail object via four approaches:
#
# * Message#field_name=(value)
# * Message#field_name(value)
# * Message#['field_name']=(value)
# * Message#[:field_name]=(value)
#
# Examples:
#
# mail = Mail.new
# mail['from'] = 'mikel@test.lindsaar.net'
# mail[:to] = 'you@test.lindsaar.net'
# mail.subject 'This is a test email'
# mail.body = 'This is a body'
#
# mail.to_s #=> "From: mikel@test.lindsaar.net\r\nTo: you@...
#
def initialize(*args, &block)
@body = nil
@body_raw = nil
@separate_parts = false
@text_part = nil
@html_part = nil
@errors = nil
@header = nil
@charset = self.class.default_charset
@defaulted_charset = true
@smtp_envelope_from = nil
@smtp_envelope_to = nil
@perform_deliveries = true
@raise_delivery_errors = true
@delivery_handler = nil
@delivery_method = Mail.delivery_method.dup
@transport_encoding = Mail::Encodings.get_encoding('7bit')
@mark_for_delete = false
if args.flatten.first.respond_to?(:each_pair)
init_with_hash(args.flatten.first)
else
init_with_string(args.flatten[0].to_s)
end
# Support both builder styles:
#
# Mail.new do
# to 'recipient@example.com'
# end
#
# and
#
# Mail.new do |m|
# m.to 'recipient@example.com'
# end
if block_given?
if block.arity.zero?
instance_eval(&block)
else
yield self
end
end
self
end
# If you assign a delivery handler, mail will call :deliver_mail on the
# object you assign to delivery_handler, it will pass itself as the
# single argument.
#
# If you define a delivery_handler, then you are responsible for the
# following actions in the delivery cycle:
#
# * Appending the mail object to Mail.deliveries as you see fit.
# * Checking the mail.perform_deliveries flag to decide if you should
# actually call :deliver! the mail object or not.
# * Checking the mail.raise_delivery_errors flag to decide if you
# should raise delivery errors if they occur.
# * Actually calling :deliver! (with the bang) on the mail object to
# get it to deliver itself.
#
# A simplest implementation of a delivery_handler would be
#
# class MyObject
#
# def initialize
# @mail = Mail.new('To: mikel@test.lindsaar.net')
# @mail.delivery_handler = self
# end
#
# attr_accessor :mail
#
# def deliver_mail(mail)
# yield
# end
# end
#
# Then doing:
#
# obj = MyObject.new
# obj.mail.deliver
#
# Would cause Mail to call obj.deliver_mail passing itself as a parameter,
# which then can just yield and let Mail do its own private do_delivery
# method.
attr_accessor :delivery_handler
# If set to false, mail will go through the motions of doing a delivery,
# but not actually call the delivery method or append the mail object to
# the Mail.deliveries collection. Useful for testing.
#
# Mail.deliveries.size #=> 0
# mail.delivery_method :smtp
# mail.perform_deliveries = false
# mail.deliver # Mail::SMTP not called here
# Mail.deliveries.size #=> 0
#
# If you want to test and query the Mail.deliveries collection to see what
# mail you sent, you should set perform_deliveries to true and use
# the :test mail delivery_method:
#
# Mail.deliveries.size #=> 0
# mail.delivery_method :test
# mail.perform_deliveries = true
# mail.deliver
# Mail.deliveries.size #=> 1
#
# This setting is ignored by mail (though still available as a flag) if you
# define a delivery_handler
attr_accessor :perform_deliveries
# If set to false, mail will silently catch and ignore any exceptions
# raised through attempting to deliver an email.
#
# This setting is ignored by mail (though still available as a flag) if you
# define a delivery_handler
attr_accessor :raise_delivery_errors
def self.default_charset; @@default_charset; end
def self.default_charset=(charset); @@default_charset = charset; end
self.default_charset = 'UTF-8'
def inform_observers
Mail.inform_observers(self)
end
def inform_interceptors
Mail.inform_interceptors(self)
end
# Delivers a mail object.
#
# Examples:
#
# mail = Mail.read('file.eml')
# mail.deliver
def deliver
inform_interceptors
if delivery_handler
delivery_handler.deliver_mail(self) { do_delivery }
else
do_delivery
end
inform_observers
self
end
# This method bypasses checking perform_deliveries and raise_delivery_errors,
# so use with caution.
#
# It still however fires off the interceptors and calls the observers callbacks if they are defined.
#
# Returns self
def deliver!
inform_interceptors
response = delivery_method.deliver!(self)
inform_observers
delivery_method.settings[:return_response] ? response : self
end
def delivery_method(method = nil, settings = {})
unless method
@delivery_method
else
@delivery_method = Configuration.instance.lookup_delivery_method(method).new(settings)
end
end
def reply(*args, &block)
self.class.new.tap do |reply|
if message_id
bracketed_message_id = "<#{message_id}>"
reply.in_reply_to = bracketed_message_id
if !references.nil?
refs = [references].flatten.map { |r| "<#{r}>" }
refs << bracketed_message_id
reply.references = refs.join(' ')
elsif !in_reply_to.nil? && !in_reply_to.kind_of?(Array)
reply.references = "<#{in_reply_to}> #{bracketed_message_id}"
end
reply.references ||= bracketed_message_id
end
if subject
reply.subject = subject =~ /^Re:/i ? subject : "Re: #{subject}"
end
if reply_to || from
reply.to = self[reply_to ? :reply_to : :from].to_s
end
if to
reply.from = self[:to].formatted.first.to_s
end
unless args.empty?
if args.flatten.first.respond_to?(:each_pair)
reply.send(:init_with_hash, args.flatten.first)
else
reply.send(:init_with_string, args.flatten[0].to_s.strip)
end
end
if block_given?
reply.instance_eval(&block)
end
end
end
# Provides the operator needed for sort et al.
#
# Compares this mail object with another mail object, this is done by date, so an
# email that is older than another will appear first.
#
# Example:
#
# mail1 = Mail.new do
# date(Time.now)
# end
# mail2 = Mail.new do
# date(Time.now - 86400) # 1 day older
# end
# [mail2, mail1].sort #=> [mail2, mail1]
def <=>(other)
if other.nil?
1
else
self.date <=> other.date
end
end
# Two emails are the same if they have the same fields and body contents. One
# gotcha here is that Mail will insert Message-IDs when calling encoded, so doing
# mail1.encoded == mail2.encoded is most probably not going to return what you think
# as the assigned Message-IDs by Mail (if not already defined as the same) will ensure
# that the two objects are unique, and this comparison will ALWAYS return false.
#
# So the == operator has been defined like so: Two messages are the same if they have
# the same content, ignoring the Message-ID field, unless BOTH emails have a defined and
# different Message-ID value, then they are false.
#
# So, in practice the == operator works like this:
#
# m1 = Mail.new("Subject: Hello\r\n\r\nHello")
# m2 = Mail.new("Subject: Hello\r\n\r\nHello")
# m1 == m2 #=> true
#
# m1 = Mail.new("Subject: Hello\r\n\r\nHello")
# m2 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
# m1 == m2 #=> true
#
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
# m2 = Mail.new("Subject: Hello\r\n\r\nHello")
# m1 == m2 #=> true
#
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
# m2 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
# m1 == m2 #=> true
#
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
# m2 = Mail.new("Message-ID: <DIFFERENT@test>\r\nSubject: Hello\r\n\r\nHello")
# m1 == m2 #=> false
def ==(other)
return false unless other.respond_to?(:encoded)
if self.message_id && other.message_id
self.encoded == other.encoded
else
dup.tap { |m| m.message_id = '<temp@test>' }.encoded ==
other.dup.tap { |m| m.message_id = '<temp@test>' }.encoded
end
end
def initialize_copy(original)
super
@header = @header.dup
end
# Provides access to the raw source of the message as it was when it
# was instantiated. This is set at initialization and so is untouched
# by the parsers or decoder / encoders
#
# Example:
#
# mail = Mail.new('This is an invalid email message')
# mail.raw_source #=> "This is an invalid email message"
def raw_source
@raw_source
end
# Sets the envelope from for the email
def set_envelope(val)
@raw_envelope = val
@envelope = Mail::Envelope.parse(val) rescue nil
end
# The raw_envelope is the From mikel@test.lindsaar.net Mon May 2 16:07:05 2009
# type field that you can see at the top of any email that has come
# from a mailbox
def raw_envelope
@raw_envelope
end
def envelope_from
@envelope ? @envelope.from : nil
end
def envelope_date
@envelope ? @envelope.date : nil
end
# Sets the header of the message object.
#
# Example:
#
# mail.header = 'To: mikel@test.lindsaar.net\r\nFrom: Bob@bob.com'
# mail.header #=> <#Mail::Header
def header=(value)
@header = Mail::Header.new(value, charset)
end
# Returns the header object of the message object. Or, if passed
# a parameter sets the value.
#
# Example:
#
# mail = Mail::Message.new('To: mikel\r\nFrom: you')
# mail.header #=> #<Mail::Header:0x13ce14 @raw_source="To: mikel\r\nFr...
#
# mail.header #=> nil
# mail.header 'To: mikel\r\nFrom: you'
# mail.header #=> #<Mail::Header:0x13ce14 @raw_source="To: mikel\r\nFr...
def header(value = nil)
value ? self.header = value : @header
end
# Provides a way to set custom headers, by passing in a hash
def headers(hash = {})
hash.each_pair do |k,v|
header[k] = v
end
end
# Returns a list of parser errors on the header, each field that had an error
# will be reparsed as an unstructured field to preserve the data inside, but
# will not be used for further processing.
#
# It returns a nested array of [field_name, value, original_error_message]
# per error found.
#
# Example:
#
# message = Mail.new("Content-Transfer-Encoding: weirdo\r\n")
# message.errors.size #=> 1
# message.errors.first[0] #=> "Content-Transfer-Encoding"
# message.errors.first[1] #=> "weirdo"
# message.errors.first[3] #=> <The original error message exception>
#
# This is a good first defence on detecting spam by the way. Some spammers send
# invalid emails to try and get email parsers to give up parsing them.
def errors
header.errors
end
# Returns the Bcc value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>'
# mail.bcc #=> ['mikel@test.lindsaar.net']
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.bcc 'Mikel <mikel@test.lindsaar.net>'
# mail.bcc #=> ['mikel@test.lindsaar.net']
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.bcc 'Mikel <mikel@test.lindsaar.net>'
# mail.bcc << 'ada@test.lindsaar.net'
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def bcc( val = nil )
default :bcc, val
end
# Sets the Bcc value of the mail object, pass in a string of the field
#
# Example:
#
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>'
# mail.bcc #=> ['mikel@test.lindsaar.net']
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def bcc=( val )
header[:bcc] = val
end
# Returns the Cc value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.cc = 'Mikel <mikel@test.lindsaar.net>'
# mail.cc #=> ['mikel@test.lindsaar.net']
# mail.cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.cc 'Mikel <mikel@test.lindsaar.net>'
# mail.cc #=> ['mikel@test.lindsaar.net']
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.cc 'Mikel <mikel@test.lindsaar.net>'
# mail.cc << 'ada@test.lindsaar.net'
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def cc( val = nil )
default :cc, val
end
# Sets the Cc value of the mail object, pass in a string of the field
#
# Example:
#
# mail.cc = 'Mikel <mikel@test.lindsaar.net>'
# mail.cc #=> ['mikel@test.lindsaar.net']
# mail.cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def cc=( val )
header[:cc] = val
end
def comments( val = nil )
default :comments, val
end
def comments=( val )
header[:comments] = val
end
def content_description( val = nil )
default :content_description, val
end
def content_description=( val )
header[:content_description] = val
end
def content_disposition( val = nil )
default :content_disposition, val
end
def content_disposition=( val )
header[:content_disposition] = val
end
def content_id( val = nil )
default :content_id, val
end
def content_id=( val )
header[:content_id] = val
end
def content_location( val = nil )
default :content_location, val
end
def content_location=( val )
header[:content_location] = val
end
def content_transfer_encoding( val = nil )
default :content_transfer_encoding, val
end
def content_transfer_encoding=( val )
header[:content_transfer_encoding] = val
end
def content_type( val = nil )
default :content_type, val
end
def content_type=( val )
header[:content_type] = val
end
def date( val = nil )
default :date, val
end
def date=( val )
header[:date] = val
end
def transport_encoding( val = nil)
if val
self.transport_encoding = val
else
@transport_encoding
end
end
def transport_encoding=( val )
@transport_encoding = Mail::Encodings.get_encoding(val)
end
# Returns the From value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.from = 'Mikel <mikel@test.lindsaar.net>'
# mail.from #=> ['mikel@test.lindsaar.net']
# mail.from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.from 'Mikel <mikel@test.lindsaar.net>'
# mail.from #=> ['mikel@test.lindsaar.net']
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.from 'Mikel <mikel@test.lindsaar.net>'
# mail.from << 'ada@test.lindsaar.net'
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def from( val = nil )
default :from, val
end
# Sets the From value of the mail object, pass in a string of the field
#
# Example:
#
# mail.from = 'Mikel <mikel@test.lindsaar.net>'
# mail.from #=> ['mikel@test.lindsaar.net']
# mail.from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def from=( val )
header[:from] = val
end
def in_reply_to( val = nil )
default :in_reply_to, val
end
def in_reply_to=( val )
header[:in_reply_to] = val
end
def keywords( val = nil )
default :keywords, val
end
def keywords=( val )
header[:keywords] = val
end
# Returns the Message-ID of the mail object. Note, per RFC 2822 the Message ID
# consists of what is INSIDE the < > usually seen in the mail header, so this method
# will return only what is inside.
#
# Example:
#
# mail.message_id = '<1234@message.id>'
# mail.message_id #=> '1234@message.id'
#
# Also allows you to set the Message-ID by passing a string as a parameter
#
# mail.message_id '<1234@message.id>'
# mail.message_id #=> '1234@message.id'
def message_id( val = nil )
default :message_id, val
end
# Sets the Message-ID. Note, per RFC 2822 the Message ID consists of what is INSIDE
# the < > usually seen in the mail header, so this method will return only what is inside.
#
# mail.message_id = '<1234@message.id>'
# mail.message_id #=> '1234@message.id'
def message_id=( val )
header[:message_id] = val
end
# Returns the MIME version of the email as a string
#
# Example:
#
# mail.mime_version = '1.0'
# mail.mime_version #=> '1.0'
#
# Also allows you to set the MIME version by passing a string as a parameter.
#
# Example:
#
# mail.mime_version '1.0'
# mail.mime_version #=> '1.0'
def mime_version( val = nil )
default :mime_version, val
end
# Sets the MIME version of the email by accepting a string
#
# Example:
#
# mail.mime_version = '1.0'
# mail.mime_version #=> '1.0'
def mime_version=( val )
header[:mime_version] = val
end
def received( val = nil )
if val
header[:received] = val
else
header[:received]
end
end
def received=( val )
header[:received] = val
end
def references( val = nil )
default :references, val
end
def references=( val )
header[:references] = val
end
# Returns the Reply-To value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>'
# mail.reply_to #=> ['mikel@test.lindsaar.net']
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.reply_to 'Mikel <mikel@test.lindsaar.net>'
# mail.reply_to #=> ['mikel@test.lindsaar.net']
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.reply_to 'Mikel <mikel@test.lindsaar.net>'
# mail.reply_to << 'ada@test.lindsaar.net'
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def reply_to( val = nil )
default :reply_to, val
end
# Sets the Reply-To value of the mail object, pass in a string of the field
#
# Example:
#
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>'
# mail.reply_to #=> ['mikel@test.lindsaar.net']
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def reply_to=( val )
header[:reply_to] = val
end
# Returns the Resent-Bcc value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.resent_bcc 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.resent_bcc 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_bcc << 'ada@test.lindsaar.net'
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def resent_bcc( val = nil )
default :resent_bcc, val
end
# Sets the Resent-Bcc value of the mail object, pass in a string of the field
#
# Example:
#
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def resent_bcc=( val )
header[:resent_bcc] = val
end
# Returns the Resent-Cc value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.resent_cc 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.resent_cc 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_cc << 'ada@test.lindsaar.net'
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def resent_cc( val = nil )
default :resent_cc, val
end
# Sets the Resent-Cc value of the mail object, pass in a string of the field
#
# Example:
#
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def resent_cc=( val )
header[:resent_cc] = val
end
def resent_date( val = nil )
default :resent_date, val
end
def resent_date=( val )
header[:resent_date] = val
end
# Returns the Resent-From value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_from #=> ['mikel@test.lindsaar.net']
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.resent_from ['Mikel <mikel@test.lindsaar.net>']
# mail.resent_from #=> 'mikel@test.lindsaar.net'
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.resent_from 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_from << 'ada@test.lindsaar.net'
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def resent_from( val = nil )
default :resent_from, val
end
# Sets the Resent-From value of the mail object, pass in a string of the field
#
# Example:
#
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_from #=> ['mikel@test.lindsaar.net']
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def resent_from=( val )
header[:resent_from] = val
end
def resent_message_id( val = nil )
default :resent_message_id, val
end
def resent_message_id=( val )
header[:resent_message_id] = val
end
# Returns the Resent-Sender value of the mail object, as a single string of an address
# spec. A sender per RFC 2822 must be a single address, so you can not append to
# this address.
#
# Example:
#
# mail.resent_sender = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.resent_sender 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
def resent_sender( val = nil )
default :resent_sender, val
end
# Sets the Resent-Sender value of the mail object, pass in a string of the field
#
# Example:
#
# mail.resent_sender = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
def resent_sender=( val )
header[:resent_sender] = val
end
# Returns the Resent-To value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_to #=> ['mikel@test.lindsaar.net']
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.resent_to 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_to #=> ['mikel@test.lindsaar.net']
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.resent_to 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_to << 'ada@test.lindsaar.net'
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def resent_to( val = nil )
default :resent_to, val
end
# Sets the Resent-To value of the mail object, pass in a string of the field
#
# Example:
#
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>'
# mail.resent_to #=> ['mikel@test.lindsaar.net']
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def resent_to=( val )
header[:resent_to] = val
end
# Returns the return path of the mail object, or sets it if you pass a string
def return_path( val = nil )
default :return_path, val
end
# Sets the return path of the object
def return_path=( val )
header[:return_path] = val
end
# Returns the Sender value of the mail object, as a single string of an address
# spec. A sender per RFC 2822 must be a single address.
#
# Example:
#
# mail.sender = 'Mikel <mikel@test.lindsaar.net>'
# mail.sender #=> 'mikel@test.lindsaar.net'
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.sender 'Mikel <mikel@test.lindsaar.net>'
# mail.sender #=> 'mikel@test.lindsaar.net'
def sender( val = nil )
default :sender, val
end
# Sets the Sender value of the mail object, pass in a string of the field
#
# Example:
#
# mail.sender = 'Mikel <mikel@test.lindsaar.net>'
# mail.sender #=> 'mikel@test.lindsaar.net'
def sender=( val )
header[:sender] = val
end
# Returns the SMTP Envelope From value of the mail object, as a single
# string of an address spec.
#
# Defaults to Return-Path, Sender, or the first From address.
#
# Example:
#
# mail.smtp_envelope_from = 'Mikel <mikel@test.lindsaar.net>'
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.smtp_envelope_from 'Mikel <mikel@test.lindsaar.net>'
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
def smtp_envelope_from( val = nil )
if val
self.smtp_envelope_from = val
else
@smtp_envelope_from || return_path || sender || from_addrs.first
end
end
# Sets the From address on the SMTP Envelope.
#
# Example:
#
# mail.smtp_envelope_from = 'Mikel <mikel@test.lindsaar.net>'
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
def smtp_envelope_from=( val )
@smtp_envelope_from = val
end
# Returns the SMTP Envelope To value of the mail object.
#
# Defaults to #destinations: To, Cc, and Bcc addresses.
#
# Example:
#
# mail.smtp_envelope_to = 'Mikel <mikel@test.lindsaar.net>'
# mail.smtp_envelope_to #=> 'mikel@test.lindsaar.net'
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.smtp_envelope_to ['Mikel <mikel@test.lindsaar.net>', 'Lindsaar <lindsaar@test.lindsaar.net>']
# mail.smtp_envelope_to #=> ['mikel@test.lindsaar.net', 'lindsaar@test.lindsaar.net']
def smtp_envelope_to( val = nil )
if val
self.smtp_envelope_to = val
else
@smtp_envelope_to || destinations
end
end
# Sets the To addresses on the SMTP Envelope.
#
# Example:
#
# mail.smtp_envelope_to = 'Mikel <mikel@test.lindsaar.net>'
# mail.smtp_envelope_to #=> 'mikel@test.lindsaar.net'
#
# mail.smtp_envelope_to = ['Mikel <mikel@test.lindsaar.net>', 'Lindsaar <lindsaar@test.lindsaar.net>']
# mail.smtp_envelope_to #=> ['mikel@test.lindsaar.net', 'lindsaar@test.lindsaar.net']
def smtp_envelope_to=( val )
@smtp_envelope_to =
case val
when Array, NilClass
val
else
[val]
end
end
# Returns the decoded value of the subject field, as a single string.
#
# Example:
#
# mail.subject = "G'Day mate"
# mail.subject #=> "G'Day mate"
# mail.subject = '=?UTF-8?Q?This_is_=E3=81=82_string?='
# mail.subject #=> "This is あ string"
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.subject "G'Day mate"
# mail.subject #=> "G'Day mate"
def subject( val = nil )
default :subject, val
end
# Sets the Subject value of the mail object, pass in a string of the field
#
# Example:
#
# mail.subject = '=?UTF-8?Q?This_is_=E3=81=82_string?='
# mail.subject #=> "This is あ string"
def subject=( val )
header[:subject] = val
end
# Returns the To value of the mail object as an array of strings of
# address specs.
#
# Example:
#
# mail.to = 'Mikel <mikel@test.lindsaar.net>'
# mail.to #=> ['mikel@test.lindsaar.net']
# mail.to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
#
# Also allows you to set the value by passing a value as a parameter
#
# Example:
#
# mail.to 'Mikel <mikel@test.lindsaar.net>'
# mail.to #=> ['mikel@test.lindsaar.net']
#
# Additionally, you can append new addresses to the returned Array like
# object.
#
# Example:
#
# mail.to 'Mikel <mikel@test.lindsaar.net>'
# mail.to << 'ada@test.lindsaar.net'
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def to( val = nil )
default :to, val
end
# Sets the To value of the mail object, pass in a string of the field
#
# Example:
#
# mail.to = 'Mikel <mikel@test.lindsaar.net>'
# mail.to #=> ['mikel@test.lindsaar.net']
# mail.to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
def to=( val )
header[:to] = val
end
# Returns the default value of the field requested as a symbol.
#
# Each header field has a :default method which returns the most common use case for
# that field, for example, the date field types will return a DateTime object when
# sent :default, the subject, or unstructured fields will return a decoded string of
# their value, the address field types will return a single addr_spec or an array of
# addr_specs if there is more than one.
def default( sym, val = nil )
if val
header[sym] = val
elsif field = header[sym]
field.default
end
end
# Sets the body object of the message object.
#
# Example:
#
# mail.body = 'This is the body'
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is the bo...
#
# You can also reset the body of an Message object by setting body to nil
#
# Example:
#
# mail.body = 'this is the body'
# mail.body.encoded #=> 'this is the body'
# mail.body = nil
# mail.body.encoded #=> ''
#
# If you try and set the body of an email that is a multipart email, then instead
# of deleting all the parts of your email, mail will add a text/plain part to
# your email:
#
# mail.add_file 'somefilename.png'
# mail.parts.length #=> 1
# mail.body = "This is a body"
# mail.parts.length #=> 2
# mail.parts.last.content_type.content_type #=> 'This is a body'
def body=(value)
body_lazy(value)
end
# Returns the body of the message object. Or, if passed
# a parameter sets the value.
#
# Example:
#
# mail = Mail::Message.new('To: mikel\r\n\r\nThis is the body')
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is the bo...
#
# mail.body 'This is another body'
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is anothe...
def body(value = nil)
if value
self.body = value
else
process_body_raw if @body_raw
@body
end
end
def body_encoding(value = nil)
if value.nil?
body.encoding
else
body.encoding = value
end
end
def body_encoding=(value)
body.encoding = value
end
# Returns the list of addresses this message should be sent to by
# collecting the addresses off the to, cc and bcc fields.
#
# Example:
#
# mail.to = 'mikel@test.lindsaar.net'
# mail.cc = 'sam@test.lindsaar.net'
# mail.bcc = 'bob@test.lindsaar.net'
# mail.destinations.length #=> 3
# mail.destinations.first #=> 'mikel@test.lindsaar.net'
def destinations
[to_addrs, cc_addrs, bcc_addrs].compact.flatten
end
# Returns an array of addresses (the encoded value) in the From field,
# if no From field, returns an empty array
def from_addrs
from ? [from].flatten : []
end
# Returns an array of addresses (the encoded value) in the To field,
# if no To field, returns an empty array
def to_addrs
to ? [to].flatten : []
end
# Returns an array of addresses (the encoded value) in the Cc field,
# if no Cc field, returns an empty array
def cc_addrs
cc ? [cc].flatten : []
end
# Returns an array of addresses (the encoded value) in the Bcc field,
# if no Bcc field, returns an empty array
def bcc_addrs
bcc ? [bcc].flatten : []
end
# Allows you to add an arbitrary header
#
# Example:
#
# mail['foo'] = '1234'
# mail['foo'].to_s #=> '1234'
def []=(name, value)
if name.to_s == 'body'
self.body = value
elsif name.to_s =~ /content[-_]type/i
header[name] = value
elsif name.to_s == 'charset'
self.charset = value
else
header[name] = value
end
end
# Allows you to read an arbitrary header
#
# Example:
#
# mail['foo'] = '1234'
# mail['foo'].to_s #=> '1234'
def [](name)
header[Utilities.underscoreize(name)]
end
# Method Missing in this implementation allows you to set any of the
# standard fields directly as you would the "to", "subject" etc.
#
# Those fields used most often (to, subject et al) are given their
# own method for ease of documentation and also to avoid the hook
# call to method missing.
#
# This will only catch the known fields listed in:
#
# Mail::Field::KNOWN_FIELDS
#
# as per RFC 2822, any ruby string or method name could pretty much
# be a field name, so we don't want to just catch ANYTHING sent to
# a message object and interpret it as a header.
#
# This method provides all three types of header call to set, read
# and explicitly set with the = operator
#
# Examples:
#
# mail.comments = 'These are some comments'
# mail.comments #=> 'These are some comments'
#
# mail.comments 'These are other comments'
# mail.comments #=> 'These are other comments'
#
#
# mail.date = 'Tue, 1 Jul 2003 10:52:37 +0200'
# mail.date.to_s #=> 'Tue, 1 Jul 2003 10:52:37 +0200'
#
# mail.date 'Tue, 1 Jul 2003 10:52:37 +0200'
# mail.date.to_s #=> 'Tue, 1 Jul 2003 10:52:37 +0200'
#
#
# mail.resent_msg_id = '<1234@resent_msg_id.lindsaar.net>'
# mail.resent_msg_id #=> '<1234@resent_msg_id.lindsaar.net>'
#
# mail.resent_msg_id '<4567@resent_msg_id.lindsaar.net>'
# mail.resent_msg_id #=> '<4567@resent_msg_id.lindsaar.net>'
def method_missing(name, *args, &block)
#:nodoc:
# Only take the structured fields, as we could take _anything_ really
# as it could become an optional field... "but therin lies the dark side"
field_name = Utilities.underscoreize(name).chomp("=")
if Mail::Field::KNOWN_FIELDS.include?(field_name)
if args.empty?
header[field_name]
else
header[field_name] = args.first
end
else
super # otherwise pass it on
end
#:startdoc:
end
# Returns an FieldList of all the fields in the header in the order that
# they appear in the header
def header_fields
header.fields
end
# Returns true if the message has a message ID field, the field may or may
# not have a value, but the field exists or not.
def has_message_id?
header.has_message_id?
end
# Returns true if the message has a Date field, the field may or may
# not have a value, but the field exists or not.
def has_date?
header.has_date?
end
# Returns true if the message has a Mime-Version field, the field may or may
# not have a value, but the field exists or not.
def has_mime_version?
header.has_mime_version?
end
def has_content_type?
tmp = header[:content_type].main_type rescue nil
!!tmp
end
def has_charset?
tmp = header[:content_type].parameters rescue nil
!!(has_content_type? && tmp && tmp['charset'])
end
def has_content_transfer_encoding?
header[:content_transfer_encoding] && Utilities.blank?(header[:content_transfer_encoding].errors)
end
# Creates a new empty Message-ID field and inserts it in the correct order
# into the Header. The MessageIdField object will automatically generate
# a unique message ID if you try and encode it or output it to_s without
# specifying a message id.
#
# It will preserve the message ID you specify if you do.
def add_message_id(msg_id_val = '')
header['message-id'] = msg_id_val
end
# Creates a new empty Date field and inserts it in the correct order
# into the Header. The DateField object will automatically generate
# DateTime.now's date if you try and encode it or output it to_s without
# specifying a date yourself.
#
# It will preserve any date you specify if you do.
def add_date(date_val = '')
header['date'] = date_val
end
# Creates a new empty Mime Version field and inserts it in the correct order
# into the Header. The MimeVersion object will automatically generate
# set itself to '1.0' if you try and encode it or output it to_s without
# specifying a version yourself.
#
# It will preserve any date you specify if you do.
def add_mime_version(ver_val = '')
header['mime-version'] = ver_val
end
# Adds a content type and charset if the body is US-ASCII
#
# Otherwise raises a warning
def add_content_type
header[:content_type] = 'text/plain'
end
# Adds a content type and charset if the body is US-ASCII
#
# Otherwise raises a warning
def add_charset
if !body.empty?
# Only give a warning if this isn't an attachment, has non US-ASCII and the user
# has not specified an encoding explicitly.
if @defaulted_charset && !body.raw_source.ascii_only? && !self.attachment?
warning = "Non US-ASCII detected and no charset defined.\nDefaulting to UTF-8, set your own if this is incorrect.\n"
warn(warning)
end
if @charset
header[:content_type].parameters['charset'] = @charset
end
end
end
# Adds a content transfer encoding
def add_content_transfer_encoding
header[:content_transfer_encoding] ||= body.default_encoding
end
# Returns the MIME media type of part we are on, this is taken from the content-type header
def mime_type
has_content_type? ? header[:content_type].string : nil rescue nil
end
# Returns the character set defined in the content type field
def charset
if @header
has_content_type? ? content_type_parameters['charset'] : @charset
else
@charset
end
end
# Sets the charset to the supplied value.
def charset=(value)
@defaulted_charset = false
@charset = value
@header.charset = value
end
# Returns the main content type
def main_type
has_content_type? ? header[:content_type].main_type : nil rescue nil
end
# Returns the sub content type
def sub_type
has_content_type? ? header[:content_type].sub_type : nil rescue nil
end
# Returns the content type parameters
def content_type_parameters
has_content_type? ? header[:content_type].parameters : nil rescue nil
end
# Returns true if the message is multipart
def multipart?
has_content_type? ? !!(main_type =~ /^multipart$/i) : false
end
# Returns true if the message is a multipart/report
def multipart_report?
multipart? && sub_type =~ /^report$/i
end
# Returns true if the message is a multipart/report; report-type=delivery-status;
def delivery_status_report?
multipart_report? && content_type_parameters['report-type'] =~ /^delivery-status$/i
end
# returns the part in a multipart/report email that has the content-type delivery-status
def delivery_status_part
unless defined? @delivery_status_part
@delivery_status_part =
if delivery_status_report?
parts.detect(&:delivery_status_report_part?)
end
end
@delivery_status_part
end
def bounced?
delivery_status_part and delivery_status_part.bounced?
end
def action
delivery_status_part and delivery_status_part.action
end
def final_recipient
delivery_status_part and delivery_status_part.final_recipient
end
def error_status
delivery_status_part and delivery_status_part.error_status
end
def diagnostic_code
delivery_status_part and delivery_status_part.diagnostic_code
end
def remote_mta
delivery_status_part and delivery_status_part.remote_mta
end
def retryable?
delivery_status_part and delivery_status_part.retryable?
end
# Returns the current boundary for this message part
def boundary
content_type_parameters ? content_type_parameters['boundary'] : nil
end
# Returns a parts list object of all the parts in the message
def parts
body.parts
end
# Returns an AttachmentsList object, which holds all of the attachments in
# the receiver object (either the entire email or a part within) and all
# of its descendants.
#
# It also allows you to add attachments to the mail object directly, like so:
#
# mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
#
# If you do this, then Mail will take the file name and work out the MIME media type
# set the Content-Type, Content-Disposition, Content-Transfer-Encoding and
# base64 encode the contents of the attachment all for you.
#
# You can also specify overrides if you want by passing a hash instead of a string:
#
# mail.attachments['filename.jpg'] = {:mime_type => 'application/x-gzip',
# :content => File.read('/path/to/filename.jpg')}
#
# If you want to use a different encoding than Base64, you can pass an encoding in,
# but then it is up to you to pass in the content pre-encoded, and don't expect
# Mail to know how to decode this data:
#
# file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
# mail.attachments['filename.jpg'] = {:mime_type => 'application/x-gzip',
# :encoding => 'SpecialEncoding',
# :content => file_content }
#
# You can also search for specific attachments:
#
# # By Filename
# mail.attachments['filename.jpg'] #=> Mail::Part object or nil
#
# # or by index
# mail.attachments[0] #=> Mail::Part (first attachment)
#
def attachments
parts.attachments
end
def has_attachments?
!attachments.empty?
end
# Accessor for html_part
def html_part(&block)
if block_given?
self.html_part = Mail::Part.new(:content_type => 'text/html', &block)
else
@html_part || find_first_mime_type('text/html')
end
end
# Accessor for text_part
def text_part(&block)
if block_given?
self.text_part = Mail::Part.new(:content_type => 'text/plain', &block)
else
@text_part || find_first_mime_type('text/plain')
end
end
# Helper to add a html part to a multipart/alternative email. If this and
# text_part are both defined in a message, then it will be a multipart/alternative
# message and set itself that way.
def html_part=(msg)
# Assign the html part and set multipart/alternative if there's a text part.
if msg
msg = Mail::Part.new(:body => msg) unless msg.kind_of?(Mail::Message)
@html_part = msg
@html_part.content_type = 'text/html' unless @html_part.has_content_type?
add_multipart_alternate_header if text_part
add_part @html_part
# If nil, delete the html part and back out of multipart/alternative.
elsif @html_part
parts.delete_if { |p| p.object_id == @html_part.object_id }
@html_part = nil
if text_part
self.content_type = nil
body.boundary = nil
end
end
end
# Helper to add a text part to a multipart/alternative email. If this and
# html_part are both defined in a message, then it will be a multipart/alternative
# message and set itself that way.
def text_part=(msg)
# Assign the text part and set multipart/alternative if there's an html part.
if msg
msg = Mail::Part.new(:body => msg) unless msg.kind_of?(Mail::Message)
@text_part = msg
@text_part.content_type = 'text/plain' unless @text_part.has_content_type?
add_multipart_alternate_header if html_part
add_part @text_part
# If nil, delete the text part and back out of multipart/alternative.
elsif @text_part
parts.delete_if { |p| p.object_id == @text_part.object_id }
@text_part = nil
if html_part
self.content_type = nil
body.boundary = nil
end
end
end
# Adds a part to the parts list or creates the part list
def add_part(part)
if !body.multipart? && !Utilities.blank?(self.body.decoded)
@text_part = Mail::Part.new('Content-Type: text/plain;')
@text_part.body = body.decoded
self.body << @text_part
add_multipart_alternate_header
end
add_boundary
self.body << part
end
# Allows you to add a part in block form to an existing mail message object
#
# Example:
#
# mail = Mail.new do
# part :content_type => "multipart/alternative", :content_disposition => "inline" do |p|
# p.part :content_type => "text/plain", :body => "test text\nline #2"
# p.part :content_type => "text/html", :body => "<b>test</b> HTML<br/>\nline #2"
# end
# end
def part(params = {})
new_part = Part.new(params)
yield new_part if block_given?
add_part(new_part)
end
# Adds a file to the message. You have two options with this method, you can
# just pass in the absolute path to the file you want and Mail will read the file,
# get the filename from the path you pass in and guess the MIME media type, or you
# can pass in the filename as a string, and pass in the file content as a blob.
#
# Example:
#
# m = Mail.new
# m.add_file('/path/to/filename.png')
#
# m = Mail.new
# m.add_file(:filename => 'filename.png', :content => File.read('/path/to/file.jpg'))
#
# Note also that if you add a file to an existing message, Mail will convert that message
# to a MIME multipart email, moving whatever plain text body you had into its own text
# plain part.
#
# Example:
#
# m = Mail.new do
# body 'this is some text'
# end
# m.multipart? #=> false
# m.add_file('/path/to/filename.png')
# m.multipart? #=> true
# m.parts.first.content_type.content_type #=> 'text/plain'
# m.parts.last.content_type.content_type #=> 'image/png'
#
# See also #attachments
def add_file(values)
convert_to_multipart unless self.multipart? || Utilities.blank?(self.body.decoded)
add_multipart_mixed_header
if values.is_a?(String)
basename = File.basename(values)
filedata = File.open(values, 'rb') { |f| f.read }
else
basename = values[:filename]
filedata = values
end
self.attachments[basename] = filedata
end
MULTIPART_CONVERSION_CONTENT_FIELDS = [ :content_description, :content_disposition, :content_transfer_encoding, :content_type ]
private_constant :MULTIPART_CONVERSION_CONTENT_FIELDS if respond_to?(:private_constant)
def convert_to_multipart
text_part = Mail::Part.new(:body => body.decoded)
MULTIPART_CONVERSION_CONTENT_FIELDS.each do |field_name|
if value = send(field_name)
writer = :"#{field_name}="
text_part.send writer, value
send writer, nil
end
end
text_part.charset = charset unless @defaulted_charset
self.body = ''
self.body << text_part
end
# Encodes the message, calls encode on all its parts, gets an email message
# ready to send
def ready_to_send!
identify_and_set_transfer_encoding
parts.each do |part|
part.transport_encoding = transport_encoding
part.ready_to_send!
end
add_required_fields
end
# Outputs an encoded string representation of the mail message including
# all headers, attachments, etc. This is an encoded email in US-ASCII,
# so it is able to be directly sent to an email server.
def encoded
ready_to_send!
buffer = header.encoded
buffer << "\r\n"
buffer << body.encoded(content_transfer_encoding)
buffer
end
def without_attachments!
if has_attachments?
parts.delete_attachments
reencoded = parts.empty? ? '' : body.encoded(content_transfer_encoding)
@body = nil # So the new parts won't be added to the existing body
self.body = reencoded
end
self
end
def to_yaml(opts = {})
hash = {}
hash['headers'] = {}
header.fields.each do |field|
hash['headers'][field.name] = field.value
end
hash['delivery_handler'] = delivery_handler.to_s if delivery_handler
hash['transport_encoding'] = transport_encoding.to_s
special_variables = [:@header, :@delivery_handler, :@transport_encoding]
if multipart?
hash['multipart_body'] = []
body.parts.map { |part| hash['multipart_body'] << part.to_yaml }
special_variables.push(:@body, :@text_part, :@html_part)
end
(instance_variables.map(&:to_sym) - special_variables).each do |var|
hash[var.to_s] = instance_variable_get(var)
end
hash.to_yaml(opts)
end
def self.from_yaml(str)
hash = YAML.load(str)
m = self.new(:headers => hash['headers'])
hash.delete('headers')
hash.each do |k,v|
case
when k == 'delivery_handler'
begin
m.delivery_handler = Object.const_get(v) unless Utilities.blank?(v)
rescue NameError
end
when k == 'transport_encoding'
m.transport_encoding(v)
when k == 'multipart_body'
v.map {|part| m.add_part Mail::Part.from_yaml(part) }
when k =~ /^@/
m.instance_variable_set(k.to_sym, v)
end
end
m
end
def self.from_hash(hash)
Mail::Message.new(hash)
end
def to_s
encoded
end
def inspect
"#<#{self.class}:#{self.object_id}, Multipart: #{multipart?}, Headers: #{header.field_summary}>"
end
def inspect_structure
inspect +
if self.multipart?
"\n" + parts.inspect_structure
else
''
end
end
def decoded
case
when self.text?
decode_body_as_text
when self.attachment?
decode_body
when !self.multipart?
body.decoded
else
raise NoMethodError, 'Can not decode an entire message, try calling #decoded on the various fields and body or parts if it is a multipart message.'
end
end
def read
if self.attachment?
decode_body
else
raise NoMethodError, 'Can not call read on a part unless it is an attachment.'
end
end
def decode_body
body.decoded
end
# Returns true if this part is an attachment,
# false otherwise.
def attachment?
!!find_attachment
end
# Returns the attachment data if there is any
def attachment
@attachment
end
# Returns the filename of the attachment
def filename
find_attachment
end
def all_parts
parts.map { |p| [p, p.all_parts] }.flatten
end
def find_first_mime_type(mt)
all_parts.detect { |p| p.mime_type == mt && !p.attachment? }
end
# Skips the deletion of this message. All other messages
# flagged for delete still will be deleted at session close (i.e. when
# #find exits). Only has an effect if you're using #find_and_delete
# or #find with :delete_after_find set to true.
def skip_deletion
@mark_for_delete = false
end
# Sets whether this message should be deleted at session close (i.e.
# after #find). Message will only be deleted if messages are retrieved
# using the #find_and_delete method, or by calling #find with
# :delete_after_find set to true.
def mark_for_delete=(value = true)
@mark_for_delete = value
end
# Returns whether message will be marked for deletion.
# If so, the message will be deleted at session close (i.e. after #find
# exits), but only if also using the #find_and_delete method, or by
# calling #find with :delete_after_find set to true.
#
# Side-note: Just to be clear, this method will return true even if
# the message hasn't yet been marked for delete on the mail server.
# However, if this method returns true, it *will be* marked on the
# server after each block yields back to #find or #find_and_delete.
def is_marked_for_delete?
return @mark_for_delete
end
def text?
has_content_type? ? !!(main_type =~ /^text$/i) : false
end
private
HEADER_SEPARATOR = /#{Constants::LAX_CRLF}#{Constants::LAX_CRLF}/
# 2.1. General Description
# A message consists of header fields (collectively called "the header
# of the message") followed, optionally, by a body. The header is a
# sequence of lines of characters with special syntax as defined in
# this standard. The body is simply a sequence of characters that
# follows the header and is separated from the header by an empty line
# (i.e., a line with nothing preceding the CRLF).
def parse_message
header_part, body_part = raw_source.lstrip.split(HEADER_SEPARATOR, 2)
self.header = header_part
self.body = body_part
end
def raw_source=(value)
@raw_source = value
end
# see comments to body=. We take data and process it lazily
def body_lazy(value)
process_body_raw if @body_raw && value
case
when value == nil || value.length<=0
@body = Mail::Body.new('')
@body_raw = nil
add_encoding_to_body
when @body && @body.multipart?
self.text_part = value
else
@body_raw = value
end
end
def process_body_raw
@body = Mail::Body.new(@body_raw)
@body_raw = nil
separate_parts if @separate_parts
add_encoding_to_body
end
def set_envelope_header
raw_string = raw_source.to_s
if match_data = raw_string.match(/\AFrom\s+([^:\s]#{Constants::TEXT}*)#{Constants::LAX_CRLF}/m)
set_envelope(match_data[1])
self.raw_source = raw_string.sub(match_data[0], "")
end
end
def separate_parts
body.split!(boundary)
end
def allowed_encodings
case mime_type
when 'message/rfc822'
[Encodings::SevenBit, Encodings::EightBit, Encodings::Binary]
end
end
def add_encoding_to_body
if has_content_transfer_encoding?
@body.encoding = content_transfer_encoding
end
end
def identify_and_set_transfer_encoding
if body
if body.multipart?
self.content_transfer_encoding = @transport_encoding
else
self.content_transfer_encoding = body.negotiate_best_encoding(@transport_encoding, allowed_encodings).to_s
end
end
end
def add_required_fields
add_required_message_fields
add_multipart_mixed_header if body.multipart?
add_content_type unless has_content_type?
add_charset if text? && !has_charset?
add_content_transfer_encoding unless has_content_transfer_encoding?
end
def add_required_message_fields
add_date unless has_date?
add_mime_version unless has_mime_version?
add_message_id unless has_message_id?
end
def add_multipart_alternate_header
header['content-type'] = ContentTypeField.with_boundary('multipart/alternative').value
body.boundary = boundary
end
def add_boundary
unless body.boundary && boundary
header['content-type'] = 'multipart/mixed' unless header['content-type']
header['content-type'].parameters[:boundary] = ContentTypeField.generate_boundary
body.boundary = boundary
end
end
def add_multipart_mixed_header
unless header['content-type']
header['content-type'] = ContentTypeField.with_boundary('multipart/mixed').value
body.boundary = boundary
end
end
def init_with_hash(hash)
passed_in_options = IndifferentHash.new(hash)
self.raw_source = ''
@header = Mail::Header.new
@body = Mail::Body.new
@body_raw = nil
# We need to store the body until last, as we need all headers added first
body_content = nil
passed_in_options.each_pair do |k,v|
k = Utilities.underscoreize(k).to_sym if k.class == String
if k == :headers
self.headers(v)
elsif k == :body
body_content = v
else
self[k] = v
end
end
if body_content
self.body = body_content
if has_content_transfer_encoding?
body.encoding = content_transfer_encoding
end
end
end
def init_with_string(string)
self.raw_source = string
set_envelope_header
parse_message
@separate_parts = multipart?
end
# Returns the filename of the attachment (if it exists) or returns nil
def find_attachment
content_type_name = header[:content_type].filename rescue nil
content_disp_name = header[:content_disposition].filename rescue nil
content_loc_name = header[:content_location].location rescue nil
case
when content_disposition && content_disp_name
filename = content_disp_name
when content_type && content_type_name
filename = content_type_name
when content_location && content_loc_name
filename = content_loc_name
else
filename = nil
end
filename = Mail::Encodings.decode_encode(filename, :decode) if filename rescue filename
filename
end
def do_delivery
begin
if perform_deliveries
delivery_method.deliver!(self)
end
rescue => e # Net::SMTP errors or sendmail pipe errors
raise e if raise_delivery_errors
end
end
def decode_body_as_text
Encodings.transcode_charset decode_body, charset, 'UTF-8'
end
end
end