Skip to content

Instantly share code, notes, and snippets.

@dsturnbull
Created January 15, 2010 03:16
Show Gist options
  • Save dsturnbull/277776 to your computer and use it in GitHub Desktop.
Save dsturnbull/277776 to your computer and use it in GitHub Desktop.
require 'rbconfig'
require 'find'
require 'ftools'
include Config
# this was adapted from rdoc's install.rb by way of Log4r
$sitedir = CONFIG["sitelibdir"]
unless $sitedir
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
$libdir = File.join(CONFIG["libdir"], "ruby", version)
$sitedir = $:.find {|x| x =~ /site_ruby/ }
if !$sitedir
$sitedir = File.join($libdir, "site_ruby")
elsif $sitedir !~ Regexp.quote(version)
$sitedir = File.join($sitedir, version)
end
end
# the actual gruntwork
Dir.chdir("lib")
Find.find("action_mailer", "action_mailer.rb") { |f|
if f[-3..-1] == ".rb"
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
else
File::makedirs(File.join($sitedir, *f.split(/\//)))
end
}
module ActionMailer
module AdvAttrAccessor #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods #:nodoc:
def adv_attr_accessor(*names)
names.each do |name|
ivar = "@#{name}"
define_method("#{name}=") do |value|
instance_variable_set(ivar, value)
end
define_method(name) do |*parameters|
raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1
if parameters.empty?
if instance_variable_names.include?(ivar)
instance_variable_get(ivar)
end
else
instance_variable_set(ivar, parameters.first)
end
end
end
end
end
end
end
require 'active_support/core_ext/class'
module ActionMailer #:nodoc:
# Action Mailer allows you to send email from your application using a mailer model and views.
#
#
# = Mailer Models
#
# To use Action Mailer, you need to create a mailer model.
#
# $ script/generate mailer Notifier
#
# The generated model inherits from ActionMailer::Base. Emails are defined by creating methods within the model which are then
# used to set variables to be used in the mail template, to change options on the mail, or
# to add attachments.
#
# Examples:
#
# class Notifier < ActionMailer::Base
# def signup_notification(recipient)
# recipients recipient.email_address_with_name
# bcc ["[email protected]", "Order Watcher <[email protected]>"]
# from "[email protected]"
# subject "New account information"
# body :account => recipient
# end
# end
#
# Mailer methods have the following configuration methods available.
#
# * <tt>recipients</tt> - Takes one or more email addresses. These addresses are where your email will be delivered to. Sets the <tt>To:</tt> header.
# * <tt>subject</tt> - The subject of your email. Sets the <tt>Subject:</tt> header.
# * <tt>from</tt> - Who the email you are sending is from. Sets the <tt>From:</tt> header.
# * <tt>cc</tt> - Takes one or more email addresses. These addresses will receive a carbon copy of your email. Sets the <tt>Cc:</tt> header.
# * <tt>bcc</tt> - Takes one or more email addresses. These addresses will receive a blind carbon copy of your email. Sets the <tt>Bcc:</tt> header.
# * <tt>reply_to</tt> - Takes one or more email addresses. These addresses will be listed as the default recipients when replying to your email. Sets the <tt>Reply-To:</tt> header.
# * <tt>sent_on</tt> - The date on which the message was sent. If not set, the header wil be set by the delivery agent.
# * <tt>content_type</tt> - Specify the content type of the message. Defaults to <tt>text/plain</tt>.
# * <tt>headers</tt> - Specify additional headers to be set for the message, e.g. <tt>headers 'X-Mail-Count' => 107370</tt>.
#
# When a <tt>headers 'return-path'</tt> is specified, that value will be used as the 'envelope from'
# address. Setting this is useful when you want delivery notifications sent to a different address than
# the one in <tt>from</tt>.
#
# The <tt>body</tt> method has special behavior. It takes a hash which generates an instance variable
# named after each key in the hash containing the value that that key points to.
#
# So, for example, <tt>body :account => recipient</tt> would result
# in an instance variable <tt>@account</tt> with the value of <tt>recipient</tt> being accessible in the
# view.
#
#
# = Mailer views
#
# Like Action Controller, each mailer class has a corresponding view directory
# in which each method of the class looks for a template with its name.
# To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same name as the method
# in your mailer model. For example, in the mailer defined above, the template at
# <tt>app/views/notifier/signup_notification.erb</tt> would be used to generate the email.
#
# Variables defined in the model are accessible as instance variables in the view.
#
# Emails by default are sent in plain text, so a sample view for our model example might look like this:
#
# Hi <%= @account.name %>,
# Thanks for joining our service! Please check back often.
#
# You can even use Action Pack helpers in these views. For example:
#
# You got a new note!
# <%= truncate(note.body, 25) %>
#
#
# = Generating URLs
#
# URLs can be generated in mailer views using <tt>url_for</tt> or named routes.
# Unlike controllers from Action Pack, the mailer instance doesn't have any context about the incoming request,
# so you'll need to provide all of the details needed to generate a URL.
#
# When using <tt>url_for</tt> you'll need to provide the <tt>:host</tt>, <tt>:controller</tt>, and <tt>:action</tt>:
#
# <%= url_for(:host => "example.com", :controller => "welcome", :action => "greeting") %>
#
# When using named routes you only need to supply the <tt>:host</tt>:
#
# <%= users_url(:host => "example.com") %>
#
# You will want to avoid using the <tt>name_of_route_path</tt> form of named routes because it doesn't make sense to
# generate relative URLs in email messages.
#
# It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt> option in
# the <tt>ActionMailer::Base.default_url_options</tt> hash as follows:
#
# ActionMailer::Base.default_url_options[:host] = "example.com"
#
# This can also be set as a configuration option in <tt>config/environment.rb</tt>:
#
# config.action_mailer.default_url_options = { :host => "example.com" }
#
# If you do decide to set a default <tt>:host</tt> for your mailers you will want to use the
# <tt>:only_path => false</tt> option when using <tt>url_for</tt>. This will ensure that absolute URLs are generated because
# the <tt>url_for</tt> view helper will, by default, generate relative URLs when a <tt>:host</tt> option isn't
# explicitly provided.
#
# = Sending mail
#
# Once a mailer action and template are defined, you can deliver your message or create it and save it
# for delivery later:
#
# Notifier.deliver_signup_notification(david) # sends the email
# mail = Notifier.create_signup_notification(david) # => a tmail object
# Notifier.deliver(mail)
#
# You never instantiate your mailer class. Rather, your delivery instance
# methods are automatically wrapped in class methods that start with the word
# <tt>deliver_</tt> followed by the name of the mailer method that you would
# like to deliver. The <tt>signup_notification</tt> method defined above is
# delivered by invoking <tt>Notifier.deliver_signup_notification</tt>.
#
#
# = HTML email
#
# To send mail as HTML, make sure your view (the <tt>.erb</tt> file) generates HTML and
# set the content type to html.
#
# class MyMailer < ActionMailer::Base
# def signup_notification(recipient)
# recipients recipient.email_address_with_name
# subject "New account information"
# from "[email protected]"
# body :account => recipient
# content_type "text/html"
# end
# end
#
#
# = Multipart email
#
# You can explicitly specify multipart messages:
#
# class ApplicationMailer < ActionMailer::Base
# def signup_notification(recipient)
# recipients recipient.email_address_with_name
# subject "New account information"
# from "[email protected]"
# content_type "multipart/alternative"
#
# part :content_type => "text/html",
# :body => render_message("signup-as-html", :account => recipient)
#
# part "text/plain" do |p|
# p.body = render_message("signup-as-plain", :account => recipient)
# p.transfer_encoding = "base64"
# end
# end
# end
#
# Multipart messages can also be used implicitly because Action Mailer will automatically
# detect and use multipart templates, where each template is named after the name of the action, followed
# by the content type. Each such detected template will be added as separate part to the message.
#
# For example, if the following templates existed:
# * signup_notification.text.plain.erb
# * signup_notification.text.html.erb
# * signup_notification.text.xml.builder
# * signup_notification.text.x-yaml.erb
#
# Each would be rendered and added as a separate part to the message,
# with the corresponding content type. The content type for the entire
# message is automatically set to <tt>multipart/alternative</tt>, which indicates
# that the email contains multiple different representations of the same email
# body. The same body hash is passed to each template.
#
# Implicit template rendering is not performed if any attachments or parts have been added to the email.
# This means that you'll have to manually add each part to the email and set the content type of the email
# to <tt>multipart/alternative</tt>.
#
# = Attachments
#
# Attachments can be added by using the +attachment+ method.
#
# Example:
#
# class ApplicationMailer < ActionMailer::Base
# # attachments
# def signup_notification(recipient)
# recipients recipient.email_address_with_name
# subject "New account information"
# from "[email protected]"
#
# attachment :content_type => "image/jpeg",
# :body => File.read("an-image.jpg")
#
# attachment "application/pdf" do |a|
# a.body = generate_your_pdf_here()
# end
# end
# end
#
#
# = Configuration options
#
# These options are specified on the class level, like <tt>ActionMailer::Base.template_root = "/my/templates"</tt>
#
# * <tt>template_root</tt> - Determines the base from which template references will be made.
#
# * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
#
# * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method:
# * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default "localhost" setting.
# * <tt>:port</tt> - On the off chance that your mail server doesn't run on port 25, you can change it.
# * <tt>:domain</tt> - If you need to specify a HELO domain, you can do it here.
# * <tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting.
# * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting.
# * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here.
# This is a symbol and one of <tt>:plain</tt>, <tt>:login</tt>, <tt>:cram_md5</tt>.
# * <tt>:enable_starttls_auto</tt> - When set to true, detects if STARTTLS is enabled in your SMTP server and starts to use it.
# It works only on Ruby >= 1.8.7 and Ruby >= 1.9. Default is true.
#
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
# * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i -t</tt>.
#
# * <tt>file_settings</tt> - Allows you to override options for the <tt>:file</tt> delivery method.
# * <tt>:location</tt> - The directory into which emails will be written. Defaults to the application <tt>tmp/mails</tt>.
#
# * <tt>raise_delivery_errors</tt> - Whether or not errors should be raised if the email fails to be delivered.
#
# * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default), <tt>:sendmail</tt>, <tt>:test</tt>,
# and <tt>:file</tt>. Or you may provide a custom delivery method object eg. MyOwnDeliveryMethodClass.new
#
# * <tt>perform_deliveries</tt> - Determines whether <tt>deliver_*</tt> methods are actually carried out. By default they are,
# but this can be turned off to help functional testing.
#
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with <tt>delivery_method :test</tt>. Most useful
# for unit and functional testing.
#
# * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also
# pick a different charset from inside a method with +charset+.
#
# * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You
# can also pick a different content type from inside a method with +content_type+.
#
# * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to <tt>1.0</tt>. You
# can also pick a different value from inside a method with +mime_version+.
#
# * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates
# which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to
# <tt>["text/html", "text/enriched", "text/plain"]</tt>. Items that appear first in the array have higher priority in the mail client
# and appear last in the mime encoded message. You can also pick a different order from inside a method with
# +implicit_parts_order+.
class Base
include AdvAttrAccessor, PartContainer, Quoting, Utils
include AbstractController::RenderingController
include AbstractController::LocalizedCache
include AbstractController::Layouts
include AbstractController::Helpers
helper MailHelper
if Object.const_defined?(:ActionController)
include ActionController::UrlWriter
end
include ActionMailer::DeprecatedBody
private_class_method :new #:nodoc:
class_inheritable_accessor :view_paths
self.view_paths = []
cattr_accessor :logger
@@raise_delivery_errors = true
cattr_accessor :raise_delivery_errors
@@perform_deliveries = true
cattr_accessor :perform_deliveries
@@deliveries = []
cattr_accessor :deliveries
@@default_charset = "utf-8"
cattr_accessor :default_charset
@@default_content_type = "text/plain"
cattr_accessor :default_content_type
@@default_mime_version = "1.0"
cattr_accessor :default_mime_version
@@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ]
cattr_accessor :default_implicit_parts_order
@@protected_instance_variables = []
cattr_reader :protected_instance_variables
# Specify the BCC addresses for the message
adv_attr_accessor :bcc
# Specify the CC addresses for the message.
adv_attr_accessor :cc
# Specify the charset to use for the message. This defaults to the
# +default_charset+ specified for ActionMailer::Base.
adv_attr_accessor :charset
# Specify the content type for the message. This defaults to <tt>text/plain</tt>
# in most cases, but can be automatically set in some situations.
adv_attr_accessor :content_type
# Specify the from address for the message.
adv_attr_accessor :from
# Specify the address (if different than the "from" address) to direct
# replies to this message.
adv_attr_accessor :reply_to
# Specify additional headers to be added to the message.
adv_attr_accessor :headers
# Specify the order in which parts should be sorted, based on content-type.
# This defaults to the value for the +default_implicit_parts_order+.
adv_attr_accessor :implicit_parts_order
# Defaults to "1.0", but may be explicitly given if needed.
adv_attr_accessor :mime_version
# The recipient addresses for the message, either as a string (for a single
# address) or an array (for multiple addresses).
adv_attr_accessor :recipients
# The date on which the message was sent. If not set (the default), the
# header will be set by the delivery agent.
adv_attr_accessor :sent_on
# Specify the subject of the message.
adv_attr_accessor :subject
# Specify the template name to use for current message. This is the "base"
# template name, without the extension or directory, and may be used to
# have multiple mailer methods share the same template.
adv_attr_accessor :template
# The mail and action_name instances referenced by this mailer.
attr_reader :mail, :action_name
# Where the response body is stored.
attr_internal :response_body
# Override the mailer name, which defaults to an inflected version of the
# mailer's class name. If you want to use a template in a non-standard
# location, you can use this to specify that location.
attr_writer :mailer_name
def mailer_name(value = nil)
if value
@mailer_name = value
else
@mailer_name || self.class.mailer_name
end
end
# Alias controller_path to mailer_name so render :partial in views work.
alias :controller_path :mailer_name
class << self
attr_writer :mailer_name
delegate :settings, :settings=, :to => ActionMailer::DeliveryMethod::File, :prefix => :file
delegate :settings, :settings=, :to => ActionMailer::DeliveryMethod::Sendmail, :prefix => :sendmail
delegate :settings, :settings=, :to => ActionMailer::DeliveryMethod::Smtp, :prefix => :smtp
def mailer_name
@mailer_name ||= name.underscore
end
def delivery_method=(method_name)
@delivery_method = ActionMailer::DeliveryMethod.lookup_method(method_name)
end
def respond_to?(method_symbol, include_private = false) #:nodoc:
matches_dynamic_method?(method_symbol) || super
end
def method_missing(method_symbol, *parameters) #:nodoc:
if match = matches_dynamic_method?(method_symbol)
case match[1]
when 'create' then new(match[2], *parameters).mail
when 'deliver' then new(match[2], *parameters).deliver!
when 'new' then nil
else super
end
else
super
end
end
# Receives a raw email, parses it into an email object, decodes it,
# instantiates a new mailer, and passes the email object to the mailer
# object's +receive+ method. If you want your mailer to be able to
# process incoming messages, you'll need to implement a +receive+
# method that accepts the email object as a parameter:
#
# class MyMailer < ActionMailer::Base
# def receive(mail)
# ...
# end
# end
def receive(raw_email)
logger.info "Received mail:\n #{raw_email}" unless logger.nil?
mail = TMail::Mail.parse(raw_email)
mail.base64_decode
new.receive(mail)
end
# Deliver the given mail object directly. This can be used to deliver
# a preconstructed mail object, like:
#
# email = MyMailer.create_some_mail(parameters)
# email.set_some_obscure_header "frobnicate"
# MyMailer.deliver(email)
def deliver(mail)
new.deliver!(mail)
end
def template_root
self.view_paths && self.view_paths.first
end
# Should template root overwrite the whole view_paths?
def template_root=(root)
self.view_paths = ActionView::Base.process_view_paths(root)
end
private
def matches_dynamic_method?(method_name) #:nodoc:
method_name = method_name.to_s
/^(create|deliver)_([_a-z]\w*)/.match(method_name) || /^(new)$/.match(method_name)
end
end
# Configure delivery method. Check ActionMailer::DeliveryMethod for more
# instructions.
superclass_delegating_reader :delivery_method
self.delivery_method = :smtp
# Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
# will be initialized according to the named method. If not, the mailer will
# remain uninitialized (useful when you only need to invoke the "receive"
# method, for instance).
def initialize(method_name=nil, *parameters) #:nodoc:
@_response_body = nil
super()
create!(method_name, *parameters) if method_name
end
# Initialize the mailer via the given +method_name+. The body will be
# rendered and a new TMail::Mail object created.
def create!(method_name, *parameters) #:nodoc:
initialize_defaults(method_name)
__send__(method_name, *parameters)
# Create e-mail parts
create_parts
# Set the subject if not set yet
@subject ||= I18n.t(:subject, :scope => [:actionmailer, mailer_name, method_name],
:default => method_name.humanize)
# build the mail object itself
@mail = create_mail
end
# Delivers a TMail::Mail object. By default, it delivers the cached mail
# object (from the <tt>create!</tt> method). If no cached mail object exists, and
# no alternate has been given as the parameter, this will fail.
def deliver!(mail = @mail)
raise "no mail object available for delivery!" unless mail
if logger
logger.info "Sent mail to #{Array(recipients).join(', ')}"
logger.debug "\n#{mail.encoded}"
end
ActiveSupport::Notifications.instrument(:deliver_mail, :mail => @mail) do
begin
self.delivery_method.perform_delivery(mail) if perform_deliveries
rescue Exception => e # Net::SMTP errors or sendmail pipe errors
raise e if raise_delivery_errors
end
end
mail
end
private
# Set up the default values for the various instance variables of this
# mailer. Subclasses may override this method to provide different
# defaults.
def initialize_defaults(method_name)
@charset ||= @@default_charset.dup
@content_type ||= @@default_content_type.dup
@implicit_parts_order ||= @@default_implicit_parts_order.dup
@mime_version ||= @@default_mime_version.dup if @@default_mime_version
@mailer_name ||= self.class.mailer_name
@template ||= method_name
@action_name = @template
@parts ||= []
@headers ||= {}
@sent_on ||= Time.now
super # Run deprecation hooks
end
def create_parts
super # Run deprecation hooks
if String === response_body
@parts.unshift Part.new(
:content_type => "text/plain",
:disposition => "inline",
:charset => charset,
:body => response_body
)
else
self.class.template_root.find_all(@template, {}, mailer_name).each do |template|
@parts << Part.new(
:content_type => template.mime_type ? template.mime_type.to_s : "text/plain",
:disposition => "inline",
:charset => charset,
:body => render_to_body(:_template => template)
)
end
if @parts.size > 1
@content_type = "multipart/alternative" if @content_type !~ /^multipart/
@parts = sort_parts(@parts, @implicit_parts_order)
end
# If this is a multipart e-mail add the mime_version if it is not
# already set.
@mime_version ||= "1.0" if [email protected]?
end
end
def sort_parts(parts, order = [])
order = order.collect { |s| s.downcase }
parts = parts.sort do |a, b|
a_ct = a.content_type.downcase
b_ct = b.content_type.downcase
a_in = order.include? a_ct
b_in = order.include? b_ct
s = case
when a_in && b_in
order.index(a_ct) <=> order.index(b_ct)
when a_in
-1
when b_in
1
else
a_ct <=> b_ct
end
# reverse the ordering because parts that come last are displayed
# first in mail clients
(s * -1)
end
parts
end
def create_mail
m = TMail::Mail.new
m.subject, = quote_any_if_necessary(charset, subject)
m.to, m.from = quote_any_address_if_necessary(charset, recipients, from)
m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil?
m.cc = quote_address_if_necessary(cc, charset) unless cc.nil?
m.reply_to = quote_address_if_necessary(reply_to, charset) unless reply_to.nil?
m.mime_version = mime_version unless mime_version.nil?
m.date = sent_on.to_time rescue sent_on if sent_on
headers.each { |k, v| m[k] = v }
real_content_type, ctype_attrs = parse_content_type
if @parts.empty?
m.set_content_type(real_content_type, nil, ctype_attrs)
m.body = normalize_new_lines(body)
elsif @parts.size == 1 && @parts.first.parts.empty?
m.set_content_type(real_content_type, nil, ctype_attrs)
m.body = normalize_new_lines(@parts.first.body)
else
@parts.each do |p|
part = (TMail::Mail === p ? p : p.to_mail(self))
m.parts << part
end
if real_content_type =~ /multipart/
ctype_attrs.delete "charset"
m.set_content_type(real_content_type, nil, ctype_attrs)
end
end
@mail = m
end
end
end
require 'tmpdir'
module ActionMailer
module DeliveryMethod
# A delivery method implementation which writes all mails to a file.
class File < Method
self.settings = {
:location => defined?(Rails) ? "#{Rails.root}/tmp/mails" : "#{Dir.tmpdir}/mails"
}
def perform_delivery(mail)
FileUtils.mkdir_p settings[:location]
(mail.to + mail.cc + mail.bcc).uniq.each do |to|
::File.open(::File.join(settings[:location], to), 'a') { |f| f.write(mail) }
end
end
end
end
end
module ActionMailer
module DeliveryMethod
# A delivery method implementation which sends via sendmail.
class Sendmail < Method
self.settings = {
:location => '/usr/sbin/sendmail',
:arguments => '-i -t'
}
def perform_delivery(mail)
sendmail_args = settings[:arguments]
sendmail_args += " -f \"#{mail['return-path']}\"" if mail['return-path']
IO.popen("#{settings[:location]} #{sendmail_args}","w+") do |sm|
sm.print(mail.encoded.gsub(/\r/, ''))
sm.flush
end
end
end
end
end
module ActionMailer
module DeliveryMethod
# A delivery method implementation which sends via smtp.
class Smtp < Method
self.settings = {
:address => "localhost",
:port => 25,
:domain => 'localhost.localdomain',
:user_name => nil,
:password => nil,
:authentication => nil,
:enable_starttls_auto => true,
}
def perform_delivery(mail)
destinations = mail.destinations
mail.ready_to_send
sender = (mail['return-path'] && mail['return-path'].spec) || mail['from']
smtp = Net::SMTP.new(settings[:address], settings[:port])
smtp.enable_starttls_auto if settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto)
smtp.start(settings[:domain], settings[:user_name], settings[:password],
settings[:authentication]) do |smtp|
smtp.sendmail(mail.encoded, sender, destinations)
end
end
end
end
end
module ActionMailer
module DeliveryMethod
# A delivery method implementation designed for testing, which just appends each record to the :deliveries array
class Test < Method
def perform_delivery(mail)
ActionMailer::Base.deliveries << mail
end
end
end
end
require "active_support/core_ext/class"
module ActionMailer
module DeliveryMethod
autoload :File, 'action_mailer/delivery_method/file'
autoload :Sendmail, 'action_mailer/delivery_method/sendmail'
autoload :Smtp, 'action_mailer/delivery_method/smtp'
autoload :Test, 'action_mailer/delivery_method/test'
# Creates a new DeliveryMethod object according to the given options.
#
# If no arguments are passed to this method, then a new
# ActionMailer::DeliveryMethod::Stmp object will be returned.
#
# If you pass a Symbol as the first argument, then a corresponding
# delivery method class under the ActionMailer::DeliveryMethod namespace
# will be created.
# For example:
#
# ActionMailer::DeliveryMethod.lookup_method(:sendmail)
# # => returns a new ActionMailer::DeliveryMethod::Sendmail object
#
# If the first argument is not a Symbol, then it will simply be returned:
#
# ActionMailer::DeliveryMethod.lookup_method(MyOwnDeliveryMethod.new)
# # => returns MyOwnDeliveryMethod.new
def self.lookup_method(delivery_method)
case delivery_method
when Symbol
method_name = delivery_method.to_s.camelize
method_class = ActionMailer::DeliveryMethod.const_get(method_name)
method_class.new
when nil # default
Smtp.new
else
delivery_method
end
end
# An abstract delivery method class. There are multiple delivery method classes.
# See the classes under the ActionMailer::DeliveryMethod, e.g.
# ActionMailer::DeliveryMethod::Smtp.
# Smtp is the default delivery method for production
# while Test is used in testing.
#
# each delivery method exposes just one method
#
# delivery_method = ActionMailer::DeliveryMethod::Smtp.new
# delivery_method.perform_delivery(mail) # send the mail via smtp
#
class Method
superclass_delegating_accessor :settings
self.settings = {}
end
end
end
module ActionMailer
# TODO Remove this module all together in a next release. Ensure that super
# hooks in ActionMailer::Base are removed as well.
module DeprecatedBody
def self.included(base)
base.class_eval do
# Define the body of the message. This is either a Hash (in which case it
# specifies the variables to pass to the template when it is rendered),
# or a string, in which case it specifies the actual text of the message.
adv_attr_accessor :body
end
end
def initialize_defaults(method_name)
@body ||= {}
end
def create_parts
if String === @body
ActiveSupport::Deprecation.warn('body is deprecated. To set the body with a text ' <<
'call render(:text => "body").', caller[0,10])
self.response_body = @body
elsif @body.is_a?(Hash) && [email protected]?
ActiveSupport::Deprecation.warn('body is deprecated. To set assigns simply ' <<
'use instance variables', caller[0,10])
@body.each { |k, v| instance_variable_set(:"@#{k}", v) }
end
end
def render(*args)
options = args.last.is_a?(Hash) ? args.last : {}
if options[:body]
ActiveSupport::Deprecation.warn(':body is deprecated. To set assigns simply ' <<
'use instance variables', caller[0,1])
options.delete(:body).each do |k, v|
instance_variable_set(:"@#{k}", v)
end
end
super
end
end
end
module MailHelper
# Uses Text::Format to take the text and format it, indented two spaces for
# each line, and wrapped at 72 columns.
def block_format(text)
formatted = text.split(/\n\r\n/).collect { |paragraph|
Text::Format.new(
:columns => 72, :first_indent => 2, :body_indent => 2, :text => paragraph
).format
}.join("\n")
# Make list points stand on their own line
formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
formatted
end
end
module ActionMailer
# Represents a subpart of an email message. It shares many similar
# attributes of ActionMailer::Base. Although you can create parts manually
# and add them to the +parts+ list of the mailer, it is easier
# to use the helper methods in ActionMailer::PartContainer.
class Part
include AdvAttrAccessor, PartContainer, Utils
# Represents the body of the part, as a string. This should not be a
# Hash (like ActionMailer::Base), but if you want a template to be rendered
# into the body of a subpart you can do it with the mailer's +render+ method
# and assign the result here.
adv_attr_accessor :body
# Specify the charset for this subpart. By default, it will be the charset
# of the containing part or mailer.
adv_attr_accessor :charset
# The content disposition of this part, typically either "inline" or
# "attachment".
adv_attr_accessor :content_disposition
# The content type of the part.
adv_attr_accessor :content_type
# The filename to use for this subpart (usually for attachments).
adv_attr_accessor :filename
# Accessor for specifying additional headers to include with this part.
adv_attr_accessor :headers
# The transfer encoding to use for this subpart, like "base64" or
# "quoted-printable".
adv_attr_accessor :transfer_encoding
# Create a new part from the given +params+ hash. The valid params keys
# correspond to the accessors.
def initialize(params)
@content_type = params[:content_type]
@content_disposition = params[:disposition] || "inline"
@charset = params[:charset]
@body = params[:body]
@filename = params[:filename]
@transfer_encoding = params[:transfer_encoding] || "quoted-printable"
@headers = params[:headers] || {}
@parts = []
end
# Convert the part to a mail object which can be included in the parts
# list of another mail object.
def to_mail(defaults)
part = TMail::Mail.new
real_content_type, ctype_attrs = parse_content_type(defaults)
if @parts.empty?
part.content_transfer_encoding = transfer_encoding || "quoted-printable"
case (transfer_encoding || "").downcase
when "base64" then
part.body = TMail::Base64.folding_encode(body)
when "quoted-printable"
part.body = [normalize_new_lines(body)].pack("M*")
else
part.body = body
end
# Always set the content_type after setting the body and or parts!
# Also don't set filename and name when there is none (like in
# non-attachment parts)
if content_disposition == "attachment"
ctype_attrs.delete "charset"
part.set_content_type(real_content_type, nil,
squish("name" => filename).merge(ctype_attrs))
part.set_content_disposition(content_disposition,
squish("filename" => filename).merge(ctype_attrs))
else
part.set_content_type(real_content_type, nil, ctype_attrs)
part.set_content_disposition(content_disposition)
end
else
if String === body
@parts.unshift Part.new(:charset => charset, :body => @body, :content_type => 'text/plain')
@body = nil
end
@parts.each do |p|
prt = (TMail::Mail === p ? p : p.to_mail(defaults))
part.parts << prt
end
if real_content_type =~ /multipart/
ctype_attrs.delete 'charset'
part.set_content_type(real_content_type, nil, ctype_attrs)
end
end
headers.each { |k,v| part[k] = v }
part
end
private
def squish(values={})
values.delete_if { |k,v| v.nil? }
end
end
end
module ActionMailer
# Accessors and helpers that ActionMailer::Base and ActionMailer::Part have
# in common. Using these helpers you can easily add subparts or attachments
# to your message:
#
# def my_mail_message(...)
# ...
# part "text/plain" do |p|
# p.body "hello, world"
# p.transfer_encoding "base64"
# end
#
# attachment "image/jpg" do |a|
# a.body = File.read("hello.jpg")
# a.filename = "hello.jpg"
# end
# end
module PartContainer
# The list of subparts of this container
attr_reader :parts
# Add a part to a multipart message, with the given content-type. The
# part itself is yielded to the block so that other properties (charset,
# body, headers, etc.) can be set on it.
def part(params)
params = {:content_type => params} if String === params
part = Part.new(params)
yield part if block_given?
@parts << part
end
# Add an attachment to a multipart message. This is simply a part with the
# content-disposition set to "attachment".
def attachment(params, &block)
params = { :content_type => params } if String === params
params = { :disposition => "attachment",
:transfer_encoding => "base64" }.merge(params)
part(params, &block)
end
private
def parse_content_type(defaults=nil)
if content_type.blank?
return defaults ?
[ defaults.content_type, { 'charset' => defaults.charset } ] :
[ nil, {} ]
end
ctype, *attrs = content_type.split(/;\s*/)
attrs = attrs.inject({}) { |h,s| k,v = s.split(/=/, 2); h[k] = v; h }
[ctype, {"charset" => charset || defaults && defaults.charset}.merge(attrs)]
end
end
end
module ActionMailer
module Quoting #:nodoc:
# Convert the given text into quoted printable format, with an instruction
# that the text be eventually interpreted in the given charset.
def quoted_printable(text, charset)
text = text.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }.
gsub( / /, "_" )
"=?#{charset}?Q?#{text}?="
end
# Convert the given character to quoted printable format, taking into
# account multi-byte characters (if executing with $KCODE="u", for instance)
def quoted_printable_encode(character)
result = ""
character.each_byte { |b| result << "=%02X" % b }
result
end
# A quick-and-dirty regexp for determining whether a string contains any
# characters that need escaping.
if !defined?(CHARS_NEEDING_QUOTING)
CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
end
# Quote the given text if it contains any "illegal" characters
def quote_if_necessary(text, charset)
text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding)
(text =~ CHARS_NEEDING_QUOTING) ?
quoted_printable(text, charset) :
text
end
# Quote any of the given strings if they contain any "illegal" characters
def quote_any_if_necessary(charset, *args)
args.map { |v| quote_if_necessary(v, charset) }
end
# Quote the given address if it needs to be. The address may be a
# regular email address, or it can be a phrase followed by an address in
# brackets. The phrase is the only part that will be quoted, and only if
# it needs to be. This allows extended characters to be used in the
# "to", "from", "cc", "bcc" and "reply-to" headers.
def quote_address_if_necessary(address, charset)
if Array === address
address.map { |a| quote_address_if_necessary(a, charset) }
elsif address =~ /^(\S.*)\s+(<.*>)$/
address = $2
phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
"\"#{phrase}\" #{address}"
else
address
end
end
# Quote any of the given addresses, if they need to be.
def quote_any_address_if_necessary(charset, *args)
args.map { |v| quote_address_if_necessary(v, charset) }
end
end
end
require 'active_support/test_case'
module ActionMailer
class NonInferrableMailerError < ::StandardError
def initialize(name)
super "Unable to determine the mailer to test from #{name}. " +
"You'll need to specify it using tests YourMailer in your " +
"test case definition"
end
end
class TestCase < ActiveSupport::TestCase
include Quoting, TestHelper
setup :initialize_test_deliveries
setup :set_expected_mail
class << self
def tests(mailer)
write_inheritable_attribute(:mailer_class, mailer)
end
def mailer_class
if mailer = read_inheritable_attribute(:mailer_class)
mailer
else
tests determine_default_mailer(name)
end
end
def determine_default_mailer(name)
name.sub(/Test$/, '').constantize
rescue NameError => e
raise NonInferrableMailerError.new(name)
end
end
protected
def initialize_test_deliveries
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
end
def set_expected_mail
@expected = TMail::Mail.new
@expected.set_content_type "text", "plain", { "charset" => charset }
@expected.mime_version = '1.0'
end
private
def charset
"utf-8"
end
def encode(subject)
quoted_printable(subject, charset)
end
def read_fixture(action)
IO.readlines(File.join(RAILS_ROOT, 'test', 'fixtures', self.class.mailer_class.name.underscore, action))
end
end
end
module ActionMailer
module TestHelper
# Asserts that the number of emails sent matches the given number.
#
# def test_emails
# assert_emails 0
# ContactMailer.deliver_contact
# assert_emails 1
# ContactMailer.deliver_contact
# assert_emails 2
# end
#
# If a block is passed, that block should cause the specified number of emails to be sent.
#
# def test_emails_again
# assert_emails 1 do
# ContactMailer.deliver_contact
# end
#
# assert_emails 2 do
# ContactMailer.deliver_contact
# ContactMailer.deliver_contact
# end
# end
def assert_emails(number)
if block_given?
original_count = ActionMailer::Base.deliveries.size
yield
new_count = ActionMailer::Base.deliveries.size
assert_equal original_count + number, new_count, "#{number} emails expected, but #{new_count - original_count} were sent"
else
assert_equal number, ActionMailer::Base.deliveries.size
end
end
# Assert that no emails have been sent.
#
# def test_emails
# assert_no_emails
# ContactMailer.deliver_contact
# assert_emails 1
# end
#
# If a block is passed, that block should not cause any emails to be sent.
#
# def test_emails_again
# assert_no_emails do
# # No emails should be sent from this block
# end
# end
#
# Note: This assertion is simply a shortcut for:
#
# assert_emails 0
def assert_no_emails(&block)
assert_emails 0, &block
end
end
end
# TODO: Deprecate this
module Test
module Unit
class TestCase
include ActionMailer::TestHelper
end
end
end
module ActionMailer
module Utils #:nodoc:
def normalize_new_lines(text)
text.to_s.gsub(/\r\n?/, "\n")
end
end
end
#--
# Text::Format for Ruby
# Version 0.63
#
# Copyright (c) 2002 - 2003 Austin Ziegler
#
# $Id: format.rb,v 1.1.1.1 2004/10/14 11:59:57 webster132 Exp $
#
# ==========================================================================
# Revision History ::
# YYYY.MM.DD Change ID Developer
# Description
# --------------------------------------------------------------------------
# 2002.10.18 Austin Ziegler
# Fixed a minor problem with tabs not being counted. Changed
# abbreviations from Hash to Array to better suit Ruby's
# capabilities. Fixed problems with the way that Array arguments
# are handled in calls to the major object types, excepting in
# Text::Format#expand and Text::Format#unexpand (these will
# probably need to be fixed).
# 2002.10.30 Austin Ziegler
# Fixed the ordering of the <=> for binary tests. Fixed
# Text::Format#expand and Text::Format#unexpand to handle array
# arguments better.
# 2003.01.24 Austin Ziegler
# Fixed a problem with Text::Format::RIGHT_FILL handling where a
# single word is larger than #columns. Removed Comparable
# capabilities (<=> doesn't make sense; == does). Added Symbol
# equivalents for the Hash initialization. Hash initialization has
# been modified so that values are set as follows (Symbols are
# highest priority; strings are middle; defaults are lowest):
# @columns = arg[:columns] || arg['columns'] || @columns
# Added #hard_margins, #split_rules, #hyphenator, and #split_words.
# 2003.02.07 Austin Ziegler
# Fixed the installer for proper case-sensitive handling.
# 2003.03.28 Austin Ziegler
# Added the ability for a hyphenator to receive the formatter
# object. Fixed a bug for strings matching /\A\s*\Z/ failing
# entirely. Fixed a test case failing under 1.6.8.
# 2003.04.04 Austin Ziegler
# Handle the case of hyphenators returning nil for first/rest.
# 2003.09.17 Austin Ziegler
# Fixed a problem where #paragraphs(" ") was raising
# NoMethodError.
#
# ==========================================================================
#++
module Text #:nodoc:
# Text::Format for Ruby is copyright 2002 - 2005 by Austin Ziegler. It
# is available under Ruby's licence, the Perl Artistic licence, or the
# GNU GPL version 2 (or at your option, any later version). As a
# special exception, for use with official Rails (provided by the
# rubyonrails.org development team) and any project created with
# official Rails, the following alternative MIT-style licence may be
# used:
#
# == Text::Format Licence for Rails and Rails Applications
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# * The names of its contributors may not be used to endorse or
# promote products derived from this software without specific prior
# written permission.
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
class Format
VERSION = '0.63'
# Local abbreviations. More can be added with Text::Format.abbreviations
ABBREV = [ 'Mr', 'Mrs', 'Ms', 'Jr', 'Sr' ]
# Formatting values
LEFT_ALIGN = 0
RIGHT_ALIGN = 1
RIGHT_FILL = 2
JUSTIFY = 3
# Word split modes (only applies when #hard_margins is true).
SPLIT_FIXED = 1
SPLIT_CONTINUATION = 2
SPLIT_HYPHENATION = 4
SPLIT_CONTINUATION_FIXED = SPLIT_CONTINUATION | SPLIT_FIXED
SPLIT_HYPHENATION_FIXED = SPLIT_HYPHENATION | SPLIT_FIXED
SPLIT_HYPHENATION_CONTINUATION = SPLIT_HYPHENATION | SPLIT_CONTINUATION
SPLIT_ALL = SPLIT_HYPHENATION | SPLIT_CONTINUATION | SPLIT_FIXED
# Words forcibly split by Text::Format will be stored as split words.
# This class represents a word forcibly split.
class SplitWord
# The word that was split.
attr_reader :word
# The first part of the word that was split.
attr_reader :first
# The remainder of the word that was split.
attr_reader :rest
def initialize(word, first, rest) #:nodoc:
@word = word
@first = first
@rest = rest
end
end
private
LEQ_RE = /[.?!]['"]?$/
def brk_re(i) #:nodoc:
%r/((?:\S+\s+){#{i}})(.+)/
end
def posint(p) #:nodoc:
p.to_i.abs
end
public
# Compares two Text::Format objects. All settings of the objects are
# compared *except* #hyphenator. Generated results (e.g., #split_words)
# are not compared, either.
def ==(o)
(@text == o.text) &&
(@columns == o.columns) &&
(@left_margin == o.left_margin) &&
(@right_margin == o.right_margin) &&
(@hard_margins == o.hard_margins) &&
(@split_rules == o.split_rules) &&
(@first_indent == o.first_indent) &&
(@body_indent == o.body_indent) &&
(@tag_text == o.tag_text) &&
(@tabstop == o.tabstop) &&
(@format_style == o.format_style) &&
(@extra_space == o.extra_space) &&
(@tag_paragraph == o.tag_paragraph) &&
(@nobreak == o.nobreak) &&
(@abbreviations == o.abbreviations) &&
(@nobreak_regex == o.nobreak_regex)
end
# The text to be manipulated. Note that value is optional, but if the
# formatting functions are called without values, this text is what will
# be formatted.
#
# *Default*:: <tt>[]</tt>
# <b>Used in</b>:: All methods
attr_accessor :text
# The total width of the format area. The margins, indentation, and text
# are formatted into this space.
#
# COLUMNS
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# left margin indent text is formatted into here right margin
#
# *Default*:: <tt>72</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
# <tt>#center</tt>
attr_reader :columns
# The total width of the format area. The margins, indentation, and text
# are formatted into this space. The value provided is silently
# converted to a positive integer.
#
# COLUMNS
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# left margin indent text is formatted into here right margin
#
# *Default*:: <tt>72</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
# <tt>#center</tt>
def columns=(c)
@columns = posint(c)
end
# The number of spaces used for the left margin.
#
# columns
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# LEFT MARGIN indent text is formatted into here right margin
#
# *Default*:: <tt>0</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
# <tt>#center</tt>
attr_reader :left_margin
# The number of spaces used for the left margin. The value provided is
# silently converted to a positive integer value.
#
# columns
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# LEFT MARGIN indent text is formatted into here right margin
#
# *Default*:: <tt>0</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
# <tt>#center</tt>
def left_margin=(left)
@left_margin = posint(left)
end
# The number of spaces used for the right margin.
#
# columns
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# left margin indent text is formatted into here RIGHT MARGIN
#
# *Default*:: <tt>0</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
# <tt>#center</tt>
attr_reader :right_margin
# The number of spaces used for the right margin. The value provided is
# silently converted to a positive integer value.
#
# columns
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# left margin indent text is formatted into here RIGHT MARGIN
#
# *Default*:: <tt>0</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
# <tt>#center</tt>
def right_margin=(r)
@right_margin = posint(r)
end
# The number of spaces to indent the first line of a paragraph.
#
# columns
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# left margin INDENT text is formatted into here right margin
#
# *Default*:: <tt>4</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_reader :first_indent
# The number of spaces to indent the first line of a paragraph. The
# value provided is silently converted to a positive integer value.
#
# columns
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# left margin INDENT text is formatted into here right margin
#
# *Default*:: <tt>4</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def first_indent=(f)
@first_indent = posint(f)
end
# The number of spaces to indent all lines after the first line of a
# paragraph.
#
# columns
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# left margin INDENT text is formatted into here right margin
#
# *Default*:: <tt>0</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_reader :body_indent
# The number of spaces to indent all lines after the first line of
# a paragraph. The value provided is silently converted to a
# positive integer value.
#
# columns
# <-------------------------------------------------------------->
# <-----------><------><---------------------------><------------>
# left margin INDENT text is formatted into here right margin
#
# *Default*:: <tt>0</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def body_indent=(b)
@body_indent = posint(b)
end
# Normally, words larger than the format area will be placed on a line
# by themselves. Setting this to +true+ will force words larger than the
# format area to be split into one or more "words" each at most the size
# of the format area. The first line and the original word will be
# placed into <tt>#split_words</tt>. Note that this will cause the
# output to look *similar* to a #format_style of JUSTIFY. (Lines will be
# filled as much as possible.)
#
# *Default*:: +false+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_accessor :hard_margins
# An array of words split during formatting if #hard_margins is set to
# +true+.
# #split_words << Text::Format::SplitWord.new(word, first, rest)
attr_reader :split_words
# The object responsible for hyphenating. It must respond to
# #hyphenate_to(word, size) or #hyphenate_to(word, size, formatter) and
# return an array of the word split into two parts; if there is a
# hyphenation mark to be applied, responsibility belongs to the
# hyphenator object. The size is the MAXIMUM size permitted, including
# any hyphenation marks. If the #hyphenate_to method has an arity of 3,
# the formatter will be provided to the method. This allows the
# hyphenator to make decisions about the hyphenation based on the
# formatting rules.
#
# *Default*:: +nil+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_reader :hyphenator
# The object responsible for hyphenating. It must respond to
# #hyphenate_to(word, size) and return an array of the word hyphenated
# into two parts. The size is the MAXIMUM size permitted, including any
# hyphenation marks.
#
# *Default*:: +nil+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def hyphenator=(h)
raise ArgumentError, "#{h.inspect} is not a valid hyphenator." unless h.respond_to?(:hyphenate_to)
arity = h.method(:hyphenate_to).arity
raise ArgumentError, "#{h.inspect} must have exactly two or three arguments." unless [2, 3].include?(arity)
@hyphenator = h
@hyphenator_arity = arity
end
# Specifies the split mode; used only when #hard_margins is set to
# +true+. Allowable values are:
# [+SPLIT_FIXED+] The word will be split at the number of
# characters needed, with no marking at all.
# repre
# senta
# ion
# [+SPLIT_CONTINUATION+] The word will be split at the number of
# characters needed, with a C-style continuation
# character. If a word is the only item on a
# line and it cannot be split into an
# appropriate size, SPLIT_FIXED will be used.
# repr\
# esen\
# tati\
# on
# [+SPLIT_HYPHENATION+] The word will be split according to the
# hyphenator specified in #hyphenator. If there
# is no #hyphenator specified, works like
# SPLIT_CONTINUATION. The example is using
# TeX::Hyphen. If a word is the only item on a
# line and it cannot be split into an
# appropriate size, SPLIT_CONTINUATION mode will
# be used.
# rep-
# re-
# sen-
# ta-
# tion
#
# *Default*:: <tt>Text::Format::SPLIT_FIXED</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_reader :split_rules
# Specifies the split mode; used only when #hard_margins is set to
# +true+. Allowable values are:
# [+SPLIT_FIXED+] The word will be split at the number of
# characters needed, with no marking at all.
# repre
# senta
# ion
# [+SPLIT_CONTINUATION+] The word will be split at the number of
# characters needed, with a C-style continuation
# character.
# repr\
# esen\
# tati\
# on
# [+SPLIT_HYPHENATION+] The word will be split according to the
# hyphenator specified in #hyphenator. If there
# is no #hyphenator specified, works like
# SPLIT_CONTINUATION. The example is using
# TeX::Hyphen as the #hyphenator.
# rep-
# re-
# sen-
# ta-
# tion
#
# These values can be bitwise ORed together (e.g., <tt>SPLIT_FIXED |
# SPLIT_CONTINUATION</tt>) to provide fallback split methods. In the
# example given, an attempt will be made to split the word using the
# rules of SPLIT_CONTINUATION; if there is not enough room, the word
# will be split with the rules of SPLIT_FIXED. These combinations are
# also available as the following values:
# * +SPLIT_CONTINUATION_FIXED+
# * +SPLIT_HYPHENATION_FIXED+
# * +SPLIT_HYPHENATION_CONTINUATION+
# * +SPLIT_ALL+
#
# *Default*:: <tt>Text::Format::SPLIT_FIXED</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def split_rules=(s)
raise ArgumentError, "Invalid value provided for split_rules." if ((s < SPLIT_FIXED) || (s > SPLIT_ALL))
@split_rules = s
end
# Indicates whether sentence terminators should be followed by a single
# space (+false+), or two spaces (+true+).
#
# *Default*:: +false+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_accessor :extra_space
# Defines the current abbreviations as an array. This is only used if
# extra_space is turned on.
#
# If one is abbreviating "President" as "Pres." (abbreviations =
# ["Pres"]), then the results of formatting will be as illustrated in
# the table below:
#
# extra_space | include? | !include?
# true | Pres. Lincoln | Pres. Lincoln
# false | Pres. Lincoln | Pres. Lincoln
#
# *Default*:: <tt>{}</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_accessor :abbreviations
# Indicates whether the formatting of paragraphs should be done with
# tagged paragraphs. Useful only with <tt>#tag_text</tt>.
#
# *Default*:: +false+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_accessor :tag_paragraph
# The array of text to be placed before each paragraph when
# <tt>#tag_paragraph</tt> is +true+. When <tt>#format()</tt> is called,
# only the first element of the array is used. When <tt>#paragraphs</tt>
# is called, then each entry in the array will be used once, with
# corresponding paragraphs. If the tag elements are exhausted before the
# text is exhausted, then the remaining paragraphs will not be tagged.
# Regardless of indentation settings, a blank line will be inserted
# between all paragraphs when <tt>#tag_paragraph</tt> is +true+.
#
# *Default*:: <tt>[]</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_accessor :tag_text
# Indicates whether or not the non-breaking space feature should be
# used.
#
# *Default*:: +false+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_accessor :nobreak
# A hash which holds the regular expressions on which spaces should not
# be broken. The hash is set up such that the key is the first word and
# the value is the second word.
#
# For example, if +nobreak_regex+ contains the following hash:
#
# { '^Mrs?\.$' => '\S+$', '^\S+$' => '^(?:S|J)r\.$'}
#
# Then "Mr. Jones", "Mrs. Jones", and "Jones Jr." would not be broken.
# If this simple matching algorithm indicates that there should not be a
# break at the current end of line, then a backtrack is done until there
# are two words on which line breaking is permitted. If two such words
# are not found, then the end of the line will be broken *regardless*.
# If there is a single word on the current line, then no backtrack is
# done and the word is stuck on the end.
#
# *Default*:: <tt>{}</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_accessor :nobreak_regex
# Indicates the number of spaces that a single tab represents.
#
# *Default*:: <tt>8</tt>
# <b>Used in</b>:: <tt>#expand</tt>, <tt>#unexpand</tt>,
# <tt>#paragraphs</tt>
attr_reader :tabstop
# Indicates the number of spaces that a single tab represents.
#
# *Default*:: <tt>8</tt>
# <b>Used in</b>:: <tt>#expand</tt>, <tt>#unexpand</tt>,
# <tt>#paragraphs</tt>
def tabstop=(t)
@tabstop = posint(t)
end
# Specifies the format style. Allowable values are:
# [+LEFT_ALIGN+] Left justified, ragged right.
# |A paragraph that is|
# |left aligned.|
# [+RIGHT_ALIGN+] Right justified, ragged left.
# |A paragraph that is|
# | right aligned.|
# [+RIGHT_FILL+] Left justified, right ragged, filled to width by
# spaces. (Essentially the same as +LEFT_ALIGN+ except
# that lines are padded on the right.)
# |A paragraph that is|
# |left aligned. |
# [+JUSTIFY+] Fully justified, words filled to width by spaces,
# except the last line.
# |A paragraph that|
# |is justified.|
#
# *Default*:: <tt>Text::Format::LEFT_ALIGN</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
attr_reader :format_style
# Specifies the format style. Allowable values are:
# [+LEFT_ALIGN+] Left justified, ragged right.
# |A paragraph that is|
# |left aligned.|
# [+RIGHT_ALIGN+] Right justified, ragged left.
# |A paragraph that is|
# | right aligned.|
# [+RIGHT_FILL+] Left justified, right ragged, filled to width by
# spaces. (Essentially the same as +LEFT_ALIGN+ except
# that lines are padded on the right.)
# |A paragraph that is|
# |left aligned. |
# [+JUSTIFY+] Fully justified, words filled to width by spaces.
# |A paragraph that|
# |is justified.|
#
# *Default*:: <tt>Text::Format::LEFT_ALIGN</tt>
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def format_style=(fs)
raise ArgumentError, "Invalid value provided for format_style." if ((fs < LEFT_ALIGN) || (fs > JUSTIFY))
@format_style = fs
end
# Indicates that the format style is left alignment.
#
# *Default*:: +true+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def left_align?
return @format_style == LEFT_ALIGN
end
# Indicates that the format style is right alignment.
#
# *Default*:: +false+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def right_align?
return @format_style == RIGHT_ALIGN
end
# Indicates that the format style is right fill.
#
# *Default*:: +false+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def right_fill?
return @format_style == RIGHT_FILL
end
# Indicates that the format style is full justification.
#
# *Default*:: +false+
# <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
def justify?
return @format_style == JUSTIFY
end
# The default implementation of #hyphenate_to implements
# SPLIT_CONTINUATION.
def hyphenate_to(word, size)
[word[0 .. (size - 2)] + "\\", word[(size - 1) .. -1]]
end
private
def __do_split_word(word, size) #:nodoc:
[word[0 .. (size - 1)], word[size .. -1]]
end
def __format(to_wrap) #:nodoc:
words = to_wrap.split(/\s+/).compact
words.shift if words[0].nil? or words[0].empty?
to_wrap = []
abbrev = false
width = @columns - @first_indent - @left_margin - @right_margin
indent_str = ' ' * @first_indent
first_line = true
line = words.shift
abbrev = __is_abbrev(line) unless line.nil? || line.empty?
while w = words.shift
if (w.size + line.size < (width - 1)) ||
((line !~ LEQ_RE || abbrev) && (w.size + line.size < width))
line << " " if (line =~ LEQ_RE) && (not abbrev)
line << " #{w}"
else
line, w = __do_break(line, w) if @nobreak
line, w = __do_hyphenate(line, w, width) if @hard_margins
if w.index(/\s+/)
w, *w2 = w.split(/\s+/)
words.unshift(w2)
words.flatten!
end
to_wrap << __make_line(line, indent_str, width, w.nil?) unless line.nil?
if first_line
first_line = false
width = @columns - @body_indent - @left_margin - @right_margin
indent_str = ' ' * @body_indent
end
line = w
end
abbrev = __is_abbrev(w) unless w.nil?
end
loop do
break if line.nil? or line.empty?
line, w = __do_hyphenate(line, w, width) if @hard_margins
to_wrap << __make_line(line, indent_str, width, w.nil?)
line = w
end
if (@tag_paragraph && (to_wrap.size > 0)) then
clr = %r{`(\w+)'}.match([caller(1)].flatten[0])[1]
clr = "" if clr.nil?
if ((not @tag_text[0].nil?) && (@tag_cur.size < 1) &&
(clr != "__paragraphs")) then
@tag_cur = @tag_text[0]
end
fchar = /(\S)/.match(to_wrap[0])[1]
white = to_wrap[0].index(fchar)
if ((white - @left_margin - 1) > @tag_cur.size) then
white = @tag_cur.size + @left_margin
to_wrap[0].gsub!(/^ {#{white}}/, "#{' ' * @left_margin}#{@tag_cur}")
else
to_wrap.unshift("#{' ' * @left_margin}#{@tag_cur}\n")
end
end
to_wrap.join('')
end
# format lines in text into paragraphs with each element of @wrap a
# paragraph; uses Text::Format.format for the formatting
def __paragraphs(to_wrap) #:nodoc:
if ((@first_indent == @body_indent) || @tag_paragraph) then
p_end = "\n"
else
p_end = ''
end
cnt = 0
ret = []
to_wrap.each do |tw|
@tag_cur = @tag_text[cnt] if @tag_paragraph
@tag_cur = '' if @tag_cur.nil?
line = __format(tw)
ret << "#{line}#{p_end}" if (not line.nil?) && (line.size > 0)
cnt += 1
end
ret[-1].chomp! unless ret.empty?
ret.join('')
end
# center text using spaces on left side to pad it out empty lines
# are preserved
def __center(to_center) #:nodoc:
tabs = 0
width = @columns - @left_margin - @right_margin
centered = []
to_center.each do |tc|
s = tc.strip
tabs = s.count("\t")
tabs = 0 if tabs.nil?
ct = ((width - s.size - (tabs * @tabstop) + tabs) / 2)
ct = (width - @left_margin - @right_margin) - ct
centered << "#{s.rjust(ct)}\n"
end
centered.join('')
end
# expand tabs to spaces should be similar to Text::Tabs::expand
def __expand(to_expand) #:nodoc:
expanded = []
to_expand.split("\n").each { |te| expanded << te.gsub(/\t/, ' ' * @tabstop) }
expanded.join('')
end
def __unexpand(to_unexpand) #:nodoc:
unexpanded = []
to_unexpand.split("\n").each { |tu| unexpanded << tu.gsub(/ {#{@tabstop}}/, "\t") }
unexpanded.join('')
end
def __is_abbrev(word) #:nodoc:
# remove period if there is one.
w = word.gsub(/\.$/, '') unless word.nil?
return true if (!@extra_space || ABBREV.include?(w) || @abbreviations.include?(w))
false
end
def __make_line(line, indent, width, last = false) #:nodoc:
lmargin = " " * @left_margin
fill = " " * (width - line.size) if right_fill? && (line.size <= width)
if (justify? && ((not line.nil?) && (not line.empty?)) && line =~ /\S+\s+\S+/ && !last)
spaces = width - line.size
words = line.split(/(\s+)/)
ws = spaces / (words.size / 2)
spaces = spaces % (words.size / 2) if ws > 0
words.reverse.each do |rw|
next if (rw =~ /^\S/)
rw.sub!(/^/, " " * ws)
next unless (spaces > 0)
rw.sub!(/^/, " ")
spaces -= 1
end
line = words.join('')
end
line = "#{lmargin}#{indent}#{line}#{fill}\n" unless line.nil?
if right_align? && (not line.nil?)
line.sub(/^/, " " * (@columns - @right_margin - (line.size - 1)))
else
line
end
end
def __do_hyphenate(line, next_line, width) #:nodoc:
rline = line.dup rescue line
rnext = next_line.dup rescue next_line
loop do
if rline.size == width
break
elsif rline.size > width
words = rline.strip.split(/\s+/)
word = words[-1].dup
size = width - rline.size + word.size
if (size <= 0)
words[-1] = nil
rline = words.join(' ').strip
rnext = "#{word} #{rnext}".strip
next
end
first = rest = nil
if ((@split_rules & SPLIT_HYPHENATION) != 0)
if @hyphenator_arity == 2
first, rest = @hyphenator.hyphenate_to(word, size)
else
first, rest = @hyphenator.hyphenate_to(word, size, self)
end
end
if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil?
first, rest = self.hyphenate_to(word, size)
end
if ((@split_rules & SPLIT_FIXED) != 0) and first.nil?
first.nil? or @split_rules == SPLIT_FIXED
first, rest = __do_split_word(word, size)
end
if first.nil?
words[-1] = nil
rest = word
else
words[-1] = first
@split_words << SplitWord.new(word, first, rest)
end
rline = words.join(' ').strip
rnext = "#{rest} #{rnext}".strip
break
else
break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty?
words = rnext.split(/\s+/)
word = words.shift
size = width - rline.size - 1
if (size <= 0)
rnext = "#{word} #{words.join(' ')}".strip
break
end
first = rest = nil
if ((@split_rules & SPLIT_HYPHENATION) != 0)
if @hyphenator_arity == 2
first, rest = @hyphenator.hyphenate_to(word, size)
else
first, rest = @hyphenator.hyphenate_to(word, size, self)
end
end
first, rest = self.hyphenate_to(word, size) if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil?
first, rest = __do_split_word(word, size) if ((@split_rules & SPLIT_FIXED) != 0) and first.nil?
if (rline.size + (first ? first.size : 0)) < width
@split_words << SplitWord.new(word, first, rest)
rline = "#{rline} #{first}".strip
rnext = "#{rest} #{words.join(' ')}".strip
end
break
end
end
[rline, rnext]
end
def __do_break(line, next_line) #:nodoc:
no_brk = false
words = []
words = line.split(/\s+/) unless line.nil?
last_word = words[-1]
@nobreak_regex.each { |k, v| no_brk = ((last_word =~ /#{k}/) and (next_line =~ /#{v}/)) }
if no_brk && words.size > 1
i = words.size
while i > 0
no_brk = false
@nobreak_regex.each { |k, v| no_brk = ((words[i + 1] =~ /#{k}/) && (words[i] =~ /#{v}/)) }
i -= 1
break if not no_brk
end
if i > 0
l = brk_re(i).match(line)
line.sub!(brk_re(i), l[1])
next_line = "#{l[2]} #{next_line}"
line.sub!(/\s+$/, '')
end
end
[line, next_line]
end
def __create(arg = nil, &block) #:nodoc:
# Format::Text.new(text-to-wrap)
@text = arg unless arg.nil?
# Defaults
@columns = 72
@tabstop = 8
@first_indent = 4
@body_indent = 0
@format_style = LEFT_ALIGN
@left_margin = 0
@right_margin = 0
@extra_space = false
@text = Array.new if @text.nil?
@tag_paragraph = false
@tag_text = Array.new
@tag_cur = ""
@abbreviations = Array.new
@nobreak = false
@nobreak_regex = Hash.new
@split_words = Array.new
@hard_margins = false
@split_rules = SPLIT_FIXED
@hyphenator = self
@hyphenator_arity = self.method(:hyphenate_to).arity
instance_eval(&block) unless block.nil?
end
public
# Formats text into a nice paragraph format. The text is separated
# into words and then reassembled a word at a time using the settings
# of this Format object. If a word is larger than the number of
# columns available for formatting, then that word will appear on the
# line by itself.
#
# If +to_wrap+ is +nil+, then the value of <tt>#text</tt> will be
# worked on.
def format(to_wrap = nil)
to_wrap = @text if to_wrap.nil?
if to_wrap.class == Array
__format(to_wrap[0])
else
__format(to_wrap)
end
end
# Considers each element of text (provided or internal) as a paragraph.
# If <tt>#first_indent</tt> is the same as <tt>#body_indent</tt>, then
# paragraphs will be separated by a single empty line in the result;
# otherwise, the paragraphs will follow immediately after each other.
# Uses <tt>#format</tt> to do the heavy lifting.
def paragraphs(to_wrap = nil)
to_wrap = @text if to_wrap.nil?
__paragraphs([to_wrap].flatten)
end
# Centers the text, preserving empty lines and tabs.
def center(to_center = nil)
to_center = @text if to_center.nil?
__center([to_center].flatten)
end
# Replaces all tab characters in the text with <tt>#tabstop</tt> spaces.
def expand(to_expand = nil)
to_expand = @text if to_expand.nil?
if to_expand.class == Array
to_expand.collect { |te| __expand(te) }
else
__expand(to_expand)
end
end
# Replaces all occurrences of <tt>#tabstop</tt> consecutive spaces
# with a tab character.
def unexpand(to_unexpand = nil)
to_unexpand = @text if to_unexpand.nil?
if to_unexpand.class == Array
to_unexpand.collect { |te| v << __unexpand(te) }
else
__unexpand(to_unexpand)
end
end
# This constructor takes advantage of a technique for Ruby object
# construction introduced by Andy Hunt and Dave Thomas (see reference),
# where optional values are set using commands in a block.
#
# Text::Format.new {
# columns = 72
# left_margin = 0
# right_margin = 0
# first_indent = 4
# body_indent = 0
# format_style = Text::Format::LEFT_ALIGN
# extra_space = false
# abbreviations = {}
# tag_paragraph = false
# tag_text = []
# nobreak = false
# nobreak_regex = {}
# tabstop = 8
# text = nil
# }
#
# As shown above, +arg+ is optional. If +arg+ is specified and is a
# +String+, then arg is used as the default value of <tt>#text</tt>.
# Alternately, an existing Text::Format object can be used or a Hash can
# be used. With all forms, a block can be specified.
#
# *Reference*:: "Object Construction and Blocks"
# <http://www.pragmaticprogrammer.com/ruby/articles/insteval.html>
#
def initialize(arg = nil, &block)
case arg
when Text::Format
__create(arg.text) do
@columns = arg.columns
@tabstop = arg.tabstop
@first_indent = arg.first_indent
@body_indent = arg.body_indent
@format_style = arg.format_style
@left_margin = arg.left_margin
@right_margin = arg.right_margin
@extra_space = arg.extra_space
@tag_paragraph = arg.tag_paragraph
@tag_text = arg.tag_text
@abbreviations = arg.abbreviations
@nobreak = arg.nobreak
@nobreak_regex = arg.nobreak_regex
@text = arg.text
@hard_margins = arg.hard_margins
@split_words = arg.split_words
@split_rules = arg.split_rules
@hyphenator = arg.hyphenator
end
instance_eval(&block) unless block.nil?
when Hash
__create do
@columns = arg[:columns] || arg['columns'] || @columns
@tabstop = arg[:tabstop] || arg['tabstop'] || @tabstop
@first_indent = arg[:first_indent] || arg['first_indent'] || @first_indent
@body_indent = arg[:body_indent] || arg['body_indent'] || @body_indent
@format_style = arg[:format_style] || arg['format_style'] || @format_style
@left_margin = arg[:left_margin] || arg['left_margin'] || @left_margin
@right_margin = arg[:right_margin] || arg['right_margin'] || @right_margin
@extra_space = arg[:extra_space] || arg['extra_space'] || @extra_space
@text = arg[:text] || arg['text'] || @text
@tag_paragraph = arg[:tag_paragraph] || arg['tag_paragraph'] || @tag_paragraph
@tag_text = arg[:tag_text] || arg['tag_text'] || @tag_text
@abbreviations = arg[:abbreviations] || arg['abbreviations'] || @abbreviations
@nobreak = arg[:nobreak] || arg['nobreak'] || @nobreak
@nobreak_regex = arg[:nobreak_regex] || arg['nobreak_regex'] || @nobreak_regex
@hard_margins = arg[:hard_margins] || arg['hard_margins'] || @hard_margins
@split_rules = arg[:split_rules] || arg['split_rules'] || @split_rules
@hyphenator = arg[:hyphenator] || arg['hyphenator'] || @hyphenator
end
instance_eval(&block) unless block.nil?
when String
__create(arg, &block)
when NilClass
__create(&block)
else
raise TypeError
end
end
end
end
if __FILE__ == $0
require 'test/unit'
class TestText__Format < Test::Unit::TestCase #:nodoc:
attr_accessor :format_o
GETTYSBURG = <<-'EOS'
Four score and seven years ago our fathers brought forth on this
continent a new nation, conceived in liberty and dedicated to the
proposition that all men are created equal. Now we are engaged in
a great civil war, testing whether that nation or any nation so
conceived and so dedicated can long endure. We are met on a great
battlefield of that war. We have come to dedicate a portion of
that field as a final resting-place for those who here gave their
lives that that nation might live. It is altogether fitting and
proper that we should do this. But in a larger sense, we cannot
dedicate, we cannot consecrate, we cannot hallow this ground.
The brave men, living and dead who struggled here have consecrated
it far above our poor power to add or detract. The world will
little note nor long remember what we say here, but it can never
forget what they did here. It is for us the living rather to be
dedicated here to the unfinished work which they who fought here
have thus far so nobly advanced. It is rather for us to be here
dedicated to the great task remaining before us--that from these
honored dead we take increased devotion to that cause for which
they gave the last full measure of devotion--that we here highly
resolve that these dead shall not have died in vain, that this
nation under God shall have a new birth of freedom, and that
government of the people, by the people, for the people shall
not perish from the earth.
-- Pres. Abraham Lincoln, 19 November 1863
EOS
FIVE_COL = "Four \nscore\nand s\neven \nyears\nago o\nur fa\nthers\nbroug\nht fo\nrth o\nn thi\ns con\ntinen\nt a n\new na\ntion,\nconce\nived \nin li\nberty\nand d\nedica\nted t\no the\npropo\nsitio\nn tha\nt all\nmen a\nre cr\neated\nequal\n. Now\nwe ar\ne eng\naged \nin a \ngreat\ncivil\nwar, \ntesti\nng wh\nether\nthat \nnatio\nn or \nany n\nation\nso co\nnceiv\ned an\nd so \ndedic\nated \ncan l\nong e\nndure\n. We \nare m\net on\na gre\nat ba\nttlef\nield \nof th\nat wa\nr. We\nhave \ncome \nto de\ndicat\ne a p\nortio\nn of \nthat \nfield\nas a \nfinal\nresti\nng-pl\nace f\nor th\nose w\nho he\nre ga\nve th\neir l\nives \nthat \nthat \nnatio\nn mig\nht li\nve. I\nt is \naltog\nether\nfitti\nng an\nd pro\nper t\nhat w\ne sho\nuld d\no thi\ns. Bu\nt in \na lar\nger s\nense,\nwe ca\nnnot \ndedic\nate, \nwe ca\nnnot \nconse\ncrate\n, we \ncanno\nt hal\nlow t\nhis g\nround\n. The\nbrave\nmen, \nlivin\ng and\ndead \nwho s\ntrugg\nled h\nere h\nave c\nonsec\nrated\nit fa\nr abo\nve ou\nr poo\nr pow\ner to\nadd o\nr det\nract.\nThe w\norld \nwill \nlittl\ne not\ne nor\nlong \nremem\nber w\nhat w\ne say\nhere,\nbut i\nt can\nnever\nforge\nt wha\nt the\ny did\nhere.\nIt is\nfor u\ns the\nlivin\ng rat\nher t\no be \ndedic\nated \nhere \nto th\ne unf\ninish\ned wo\nrk wh\nich t\nhey w\nho fo\nught \nhere \nhave \nthus \nfar s\no nob\nly ad\nvance\nd. It\nis ra\nther \nfor u\ns to \nbe he\nre de\ndicat\ned to\nthe g\nreat \ntask \nremai\nning \nbefor\ne us-\n-that\nfrom \nthese\nhonor\ned de\nad we\ntake \nincre\nased \ndevot\nion t\no tha\nt cau\nse fo\nr whi\nch th\ney ga\nve th\ne las\nt ful\nl mea\nsure \nof de\nvotio\nn--th\nat we\nhere \nhighl\ny res\nolve \nthat \nthese\ndead \nshall\nnot h\nave d\nied i\nn vai\nn, th\nat th\nis na\ntion \nunder\nGod s\nhall \nhave \na new\nbirth\nof fr\needom\n, and\nthat \ngover\nnment\nof th\ne peo\nple, \nby th\ne peo\nple, \nfor t\nhe pe\nople \nshall\nnot p\nerish\nfrom \nthe e\narth.\n-- Pr\nes. A\nbraha\nm Lin\ncoln,\n19 No\nvembe\nr 186\n3 \n"
FIVE_CNT = "Four \nscore\nand \nseven\nyears\nago \nour \nfath\\\ners \nbrou\\\nght \nforth\non t\\\nhis \ncont\\\ninent\na new\nnati\\\non, \nconc\\\neived\nin l\\\niber\\\nty a\\\nnd d\\\nedic\\\nated \nto t\\\nhe p\\\nropo\\\nsiti\\\non t\\\nhat \nall \nmen \nare \ncrea\\\nted \nequa\\\nl. N\\\now we\nare \nenga\\\nged \nin a \ngreat\ncivil\nwar, \ntest\\\ning \nwhet\\\nher \nthat \nnati\\\non or\nany \nnati\\\non so\nconc\\\neived\nand \nso d\\\nedic\\\nated \ncan \nlong \nendu\\\nre. \nWe a\\\nre m\\\net on\na gr\\\neat \nbatt\\\nlefi\\\neld \nof t\\\nhat \nwar. \nWe h\\\nave \ncome \nto d\\\nedic\\\nate a\nport\\\nion \nof t\\\nhat \nfield\nas a \nfinal\nrest\\\ning-\\\nplace\nfor \nthose\nwho \nhere \ngave \ntheir\nlives\nthat \nthat \nnati\\\non m\\\night \nlive.\nIt is\nalto\\\ngeth\\\ner f\\\nitti\\\nng a\\\nnd p\\\nroper\nthat \nwe s\\\nhould\ndo t\\\nhis. \nBut \nin a \nlarg\\\ner s\\\nense,\nwe c\\\nannot\ndedi\\\ncate,\nwe c\\\nannot\ncons\\\necra\\\nte, \nwe c\\\nannot\nhall\\\now t\\\nhis \ngrou\\\nnd. \nThe \nbrave\nmen, \nlivi\\\nng a\\\nnd d\\\nead \nwho \nstru\\\nggled\nhere \nhave \ncons\\\necra\\\nted \nit f\\\nar a\\\nbove \nour \npoor \npower\nto a\\\ndd or\ndetr\\\nact. \nThe \nworld\nwill \nlitt\\\nle n\\\note \nnor \nlong \nreme\\\nmber \nwhat \nwe s\\\nay h\\\nere, \nbut \nit c\\\nan n\\\never \nforg\\\net w\\\nhat \nthey \ndid \nhere.\nIt is\nfor \nus t\\\nhe l\\\niving\nrath\\\ner to\nbe d\\\nedic\\\nated \nhere \nto t\\\nhe u\\\nnfin\\\nished\nwork \nwhich\nthey \nwho \nfoug\\\nht h\\\nere \nhave \nthus \nfar \nso n\\\nobly \nadva\\\nnced.\nIt is\nrath\\\ner f\\\nor us\nto be\nhere \ndedi\\\ncated\nto t\\\nhe g\\\nreat \ntask \nrema\\\nining\nbefo\\\nre u\\\ns--t\\\nhat \nfrom \nthese\nhono\\\nred \ndead \nwe t\\\nake \nincr\\\neased\ndevo\\\ntion \nto t\\\nhat \ncause\nfor \nwhich\nthey \ngave \nthe \nlast \nfull \nmeas\\\nure \nof d\\\nevot\\\nion-\\\n-that\nwe h\\\nere \nhigh\\\nly r\\\nesol\\\nve t\\\nhat \nthese\ndead \nshall\nnot \nhave \ndied \nin v\\\nain, \nthat \nthis \nnati\\\non u\\\nnder \nGod \nshall\nhave \na new\nbirth\nof f\\\nreed\\\nom, \nand \nthat \ngove\\\nrnme\\\nnt of\nthe \npeop\\\nle, \nby t\\\nhe p\\\neopl\\\ne, f\\\nor t\\\nhe p\\\neople\nshall\nnot \nperi\\\nsh f\\\nrom \nthe \neart\\\nh. --\nPres.\nAbra\\\nham \nLinc\\\noln, \n19 N\\\novem\\\nber \n1863 \n"
# Tests both abbreviations and abbreviations=
def test_abbreviations
abbr = [" Pres. Abraham Lincoln\n", " Pres. Abraham Lincoln\n"]
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal([], @format_o.abbreviations)
assert_nothing_raised { @format_o.abbreviations = [ 'foo', 'bar' ] }
assert_equal([ 'foo', 'bar' ], @format_o.abbreviations)
assert_equal(abbr[0], @format_o.format(abbr[0]))
assert_nothing_raised { @format_o.extra_space = true }
assert_equal(abbr[1], @format_o.format(abbr[0]))
assert_nothing_raised { @format_o.abbreviations = [ "Pres" ] }
assert_equal([ "Pres" ], @format_o.abbreviations)
assert_equal(abbr[0], @format_o.format(abbr[0]))
assert_nothing_raised { @format_o.extra_space = false }
assert_equal(abbr[0], @format_o.format(abbr[0]))
end
# Tests both body_indent and body_indent=
def test_body_indent
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal(0, @format_o.body_indent)
assert_nothing_raised { @format_o.body_indent = 7 }
assert_equal(7, @format_o.body_indent)
assert_nothing_raised { @format_o.body_indent = -3 }
assert_equal(3, @format_o.body_indent)
assert_nothing_raised { @format_o.body_indent = "9" }
assert_equal(9, @format_o.body_indent)
assert_nothing_raised { @format_o.body_indent = "-2" }
assert_equal(2, @format_o.body_indent)
assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[1])
end
# Tests both columns and columns=
def test_columns
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal(72, @format_o.columns)
assert_nothing_raised { @format_o.columns = 7 }
assert_equal(7, @format_o.columns)
assert_nothing_raised { @format_o.columns = -3 }
assert_equal(3, @format_o.columns)
assert_nothing_raised { @format_o.columns = "9" }
assert_equal(9, @format_o.columns)
assert_nothing_raised { @format_o.columns = "-2" }
assert_equal(2, @format_o.columns)
assert_nothing_raised { @format_o.columns = 40 }
assert_equal(40, @format_o.columns)
assert_match(/this continent$/,
@format_o.format(GETTYSBURG).split("\n")[1])
end
# Tests both extra_space and extra_space=
def test_extra_space
assert_nothing_raised { @format_o = Text::Format.new }
assert(!@format_o.extra_space)
assert_nothing_raised { @format_o.extra_space = true }
assert(@format_o.extra_space)
# The behaviour of extra_space is tested in test_abbreviations. There
# is no need to reproduce it here.
end
# Tests both first_indent and first_indent=
def test_first_indent
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal(4, @format_o.first_indent)
assert_nothing_raised { @format_o.first_indent = 7 }
assert_equal(7, @format_o.first_indent)
assert_nothing_raised { @format_o.first_indent = -3 }
assert_equal(3, @format_o.first_indent)
assert_nothing_raised { @format_o.first_indent = "9" }
assert_equal(9, @format_o.first_indent)
assert_nothing_raised { @format_o.first_indent = "-2" }
assert_equal(2, @format_o.first_indent)
assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[0])
end
def test_format_style
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal(Text::Format::LEFT_ALIGN, @format_o.format_style)
assert_match(/^November 1863$/,
@format_o.format(GETTYSBURG).split("\n")[-1])
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_ALIGN
}
assert_equal(Text::Format::RIGHT_ALIGN, @format_o.format_style)
assert_match(/^ +November 1863$/,
@format_o.format(GETTYSBURG).split("\n")[-1])
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_FILL
}
assert_equal(Text::Format::RIGHT_FILL, @format_o.format_style)
assert_match(/^November 1863 +$/,
@format_o.format(GETTYSBURG).split("\n")[-1])
assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
assert_equal(Text::Format::JUSTIFY, @format_o.format_style)
assert_match(/^of freedom, and that government of the people, by the people, for the$/,
@format_o.format(GETTYSBURG).split("\n")[-3])
assert_raise(ArgumentError) { @format_o.format_style = 33 }
end
def test_tag_paragraph
assert_nothing_raised { @format_o = Text::Format.new }
assert(!@format_o.tag_paragraph)
assert_nothing_raised { @format_o.tag_paragraph = true }
assert(@format_o.tag_paragraph)
assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]),
Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG]))
end
def test_tag_text
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal([], @format_o.tag_text)
assert_equal(@format_o.format(GETTYSBURG),
Text::Format.new.format(GETTYSBURG))
assert_nothing_raised {
@format_o.tag_paragraph = true
@format_o.tag_text = ["Gettysburg Address", "---"]
}
assert_not_equal(@format_o.format(GETTYSBURG),
Text::Format.new.format(GETTYSBURG))
assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]),
Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG]))
assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG,
GETTYSBURG]),
Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG,
GETTYSBURG]))
end
def test_justify?
assert_nothing_raised { @format_o = Text::Format.new }
assert(!@format_o.justify?)
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_ALIGN
}
assert(!@format_o.justify?)
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_FILL
}
assert(!@format_o.justify?)
assert_nothing_raised {
@format_o.format_style = Text::Format::JUSTIFY
}
assert(@format_o.justify?)
# The format testing is done in test_format_style
end
def test_left_align?
assert_nothing_raised { @format_o = Text::Format.new }
assert(@format_o.left_align?)
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_ALIGN
}
assert(!@format_o.left_align?)
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_FILL
}
assert(!@format_o.left_align?)
assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
assert(!@format_o.left_align?)
# The format testing is done in test_format_style
end
def test_left_margin
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal(0, @format_o.left_margin)
assert_nothing_raised { @format_o.left_margin = -3 }
assert_equal(3, @format_o.left_margin)
assert_nothing_raised { @format_o.left_margin = "9" }
assert_equal(9, @format_o.left_margin)
assert_nothing_raised { @format_o.left_margin = "-2" }
assert_equal(2, @format_o.left_margin)
assert_nothing_raised { @format_o.left_margin = 7 }
assert_equal(7, @format_o.left_margin)
assert_nothing_raised {
ft = @format_o.format(GETTYSBURG).split("\n")
assert_match(/^ {11}Four score/, ft[0])
assert_match(/^ {7}November/, ft[-1])
}
end
def test_hard_margins
assert_nothing_raised { @format_o = Text::Format.new }
assert(!@format_o.hard_margins)
assert_nothing_raised {
@format_o.hard_margins = true
@format_o.columns = 5
@format_o.first_indent = 0
@format_o.format_style = Text::Format::RIGHT_FILL
}
assert(@format_o.hard_margins)
assert_equal(FIVE_COL, @format_o.format(GETTYSBURG))
assert_nothing_raised {
@format_o.split_rules |= Text::Format::SPLIT_CONTINUATION
assert_equal(Text::Format::SPLIT_CONTINUATION_FIXED,
@format_o.split_rules)
}
assert_equal(FIVE_CNT, @format_o.format(GETTYSBURG))
end
# Tests both nobreak and nobreak_regex, since one is only useful
# with the other.
def test_nobreak
assert_nothing_raised { @format_o = Text::Format.new }
assert(!@format_o.nobreak)
assert(@format_o.nobreak_regex.empty?)
assert_nothing_raised {
@format_o.nobreak = true
@format_o.nobreak_regex = { '^this$' => '^continent$' }
@format_o.columns = 77
}
assert(@format_o.nobreak)
assert_equal({ '^this$' => '^continent$' }, @format_o.nobreak_regex)
assert_match(/^this continent/,
@format_o.format(GETTYSBURG).split("\n")[1])
end
def test_right_align?
assert_nothing_raised { @format_o = Text::Format.new }
assert(!@format_o.right_align?)
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_ALIGN
}
assert(@format_o.right_align?)
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_FILL
}
assert(!@format_o.right_align?)
assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
assert(!@format_o.right_align?)
# The format testing is done in test_format_style
end
def test_right_fill?
assert_nothing_raised { @format_o = Text::Format.new }
assert(!@format_o.right_fill?)
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_ALIGN
}
assert(!@format_o.right_fill?)
assert_nothing_raised {
@format_o.format_style = Text::Format::RIGHT_FILL
}
assert(@format_o.right_fill?)
assert_nothing_raised {
@format_o.format_style = Text::Format::JUSTIFY
}
assert(!@format_o.right_fill?)
# The format testing is done in test_format_style
end
def test_right_margin
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal(0, @format_o.right_margin)
assert_nothing_raised { @format_o.right_margin = -3 }
assert_equal(3, @format_o.right_margin)
assert_nothing_raised { @format_o.right_margin = "9" }
assert_equal(9, @format_o.right_margin)
assert_nothing_raised { @format_o.right_margin = "-2" }
assert_equal(2, @format_o.right_margin)
assert_nothing_raised { @format_o.right_margin = 7 }
assert_equal(7, @format_o.right_margin)
assert_nothing_raised {
ft = @format_o.format(GETTYSBURG).split("\n")
assert_match(/^ {4}Four score.*forth on$/, ft[0])
assert_match(/^November/, ft[-1])
}
end
def test_tabstop
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal(8, @format_o.tabstop)
assert_nothing_raised { @format_o.tabstop = 7 }
assert_equal(7, @format_o.tabstop)
assert_nothing_raised { @format_o.tabstop = -3 }
assert_equal(3, @format_o.tabstop)
assert_nothing_raised { @format_o.tabstop = "9" }
assert_equal(9, @format_o.tabstop)
assert_nothing_raised { @format_o.tabstop = "-2" }
assert_equal(2, @format_o.tabstop)
end
def test_text
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal([], @format_o.text)
assert_nothing_raised { @format_o.text = "Test Text" }
assert_equal("Test Text", @format_o.text)
assert_nothing_raised { @format_o.text = ["Line 1", "Line 2"] }
assert_equal(["Line 1", "Line 2"], @format_o.text)
end
def test_s_new
# new(NilClass) { block }
assert_nothing_raised do
@format_o = Text::Format.new {
self.text = "Test 1, 2, 3"
}
end
assert_equal("Test 1, 2, 3", @format_o.text)
# new(Hash Symbols)
assert_nothing_raised { @format_o = Text::Format.new(:columns => 72) }
assert_equal(72, @format_o.columns)
# new(Hash String)
assert_nothing_raised { @format_o = Text::Format.new('columns' => 72) }
assert_equal(72, @format_o.columns)
# new(Hash) { block }
assert_nothing_raised do
@format_o = Text::Format.new('columns' => 80) {
self.text = "Test 4, 5, 6"
}
end
assert_equal("Test 4, 5, 6", @format_o.text)
assert_equal(80, @format_o.columns)
# new(Text::Format)
assert_nothing_raised do
fo = Text::Format.new(@format_o)
assert(fo == @format_o)
end
# new(Text::Format) { block }
assert_nothing_raised do
fo = Text::Format.new(@format_o) { self.columns = 79 }
assert(fo != @format_o)
end
# new(String)
assert_nothing_raised { @format_o = Text::Format.new("Test A, B, C") }
assert_equal("Test A, B, C", @format_o.text)
# new(String) { block }
assert_nothing_raised do
@format_o = Text::Format.new("Test X, Y, Z") { self.columns = -5 }
end
assert_equal("Test X, Y, Z", @format_o.text)
assert_equal(5, @format_o.columns)
end
def test_center
assert_nothing_raised { @format_o = Text::Format.new }
assert_nothing_raised do
ct = @format_o.center(GETTYSBURG.split("\n")).split("\n")
assert_match(/^ Four score and seven years ago our fathers brought forth on this/, ct[0])
assert_match(/^ not perish from the earth./, ct[-3])
end
end
def test_expand
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal(" ", @format_o.expand("\t "))
assert_nothing_raised { @format_o.tabstop = 4 }
assert_equal(" ", @format_o.expand("\t "))
end
def test_unexpand
assert_nothing_raised { @format_o = Text::Format.new }
assert_equal("\t ", @format_o.unexpand(" "))
assert_nothing_raised { @format_o.tabstop = 4 }
assert_equal("\t ", @format_o.unexpand(" "))
end
def test_space_only
assert_equal("", Text::Format.new.format(" "))
assert_equal("", Text::Format.new.format("\n"))
assert_equal("", Text::Format.new.format(" "))
assert_equal("", Text::Format.new.format(" \n"))
assert_equal("", Text::Format.new.paragraphs("\n"))
assert_equal("", Text::Format.new.paragraphs(" "))
assert_equal("", Text::Format.new.paragraphs(" "))
assert_equal("", Text::Format.new.paragraphs(" \n"))
assert_equal("", Text::Format.new.paragraphs(["\n"]))
assert_equal("", Text::Format.new.paragraphs([" "]))
assert_equal("", Text::Format.new.paragraphs([" "]))
assert_equal("", Text::Format.new.paragraphs([" \n"]))
end
def test_splendiferous
h = nil
test = "This is a splendiferous test"
assert_nothing_raised { @format_o = Text::Format.new(:columns => 6, :left_margin => 0, :indent => 0, :first_indent => 0) }
assert_match(/^splendiferous$/, @format_o.format(test))
assert_nothing_raised { @format_o.hard_margins = true }
assert_match(/^lendif$/, @format_o.format(test))
assert_nothing_raised { h = Object.new }
assert_nothing_raised do
@format_o.split_rules = Text::Format::SPLIT_HYPHENATION
class << h #:nodoc:
def hyphenate_to(word, size)
return ["", word] if size < 2
[word[0 ... size], word[size .. -1]]
end
end
@format_o.hyphenator = h
end
assert_match(/^iferou$/, @format_o.format(test))
assert_nothing_raised { h = Object.new }
assert_nothing_raised do
class << h #:nodoc:
def hyphenate_to(word, size, formatter)
return ["", word] if word.size < formatter.columns
[word[0 ... size], word[size .. -1]]
end
end
@format_o.hyphenator = h
end
assert_match(/^ferous$/, @format_o.format(test))
end
end
end
# Prefer gems to the bundled libs.
require 'rubygems'
begin
gem 'text-format', '>= 0.6.3'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/text-format-0.6.3"
end
require 'text/format'
=begin rdoc
= Address handling class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/encode'
require 'tmail/parser'
module TMail
# = Class Address
#
# Provides a complete handling library for email addresses. Can parse a string of an
# address directly or take in preformatted addresses themselves. Allows you to add
# and remove phrases from the front of the address and provides a compare function for
# email addresses.
#
# == Parsing and Handling a Valid Address:
#
# Just pass the email address in as a string to Address.parse:
#
# email = TMail::Address.parse('Mikel Lindsaar <[email protected]>)
# #=> #<TMail::Address [email protected]>
# email.address
# #=> "[email protected]"
# email.local
# #=> "mikel"
# email.domain
# #=> "lindsaar.net"
# email.name # Aliased as phrase as well
# #=> "Mikel Lindsaar"
#
# == Detecting an Invalid Address
#
# If you want to check the syntactical validity of an email address, just pass it to
# Address.parse and catch any SyntaxError:
#
# begin
# TMail::Mail.parse("mikel 2@@@@@ me .com")
# rescue TMail::SyntaxError
# puts("Invalid Email Address Detected")
# else
# puts("Address is valid")
# end
# #=> "Invalid Email Address Detected"
class Address
include TextUtils #:nodoc:
# Sometimes you need to parse an address, TMail can do it for you and provide you with
# a fairly robust method of detecting a valid address.
#
# Takes in a string, returns a TMail::Address object.
#
# Raises a TMail::SyntaxError on invalid email format
def Address.parse( str )
Parser.parse :ADDRESS, special_quote_address(str)
end
def Address.special_quote_address(str) #:nodoc:
# Takes a string which is an address and adds quotation marks to special
# edge case methods that the RACC parser can not handle.
#
# Right now just handles two edge cases:
#
# Full stop as the last character of the display name:
# Mikel L. <[email protected]>
# Returns:
# "Mikel L." <[email protected]>
#
# Unquoted @ symbol in the display name:
# [email protected] <[email protected]>
# Returns:
# "[email protected]" <[email protected]>
#
# Any other address not matching these patterns just gets returned as is.
case
# This handles the missing "" in an older version of Apple Mail.app
# around the display name when the display name contains a '@'
# like '[email protected] <[email protected]>'
# Just quotes it to: '"[email protected]" <[email protected]>'
when str =~ /\A([^"].+@.+[^"])\s(<.*?>)\Z/
return "\"#{$1}\" #{$2}"
# This handles cases where 'Mikel A. <[email protected]>' which is a trailing
# full stop before the address section. Just quotes it to
# '"Mikel A. <[email protected]>"
when str =~ /\A(.*?\.)\s(<.*?>)\Z/
return "\"#{$1}\" #{$2}"
else
str
end
end
def address_group? #:nodoc:
false
end
# Address.new(local, domain)
#
# Accepts:
#
# * local - Left of the at symbol
#
# * domain - Array of the domain split at the periods.
#
# For example:
#
# Address.new("mikel", ["lindsaar", "net"])
# #=> "#<TMail::Address [email protected]>"
def initialize( local, domain )
if domain
domain.each do |s|
raise SyntaxError, 'empty word in domain' if s.empty?
end
end
# This is to catch an unquoted "@" symbol in the local part of the
# address. Handles addresses like <"@"@me.com> and makes sure they
# stay like <"@"@me.com> (previously were becoming <@@me.com>)
if local && (local.join == '@' || local.join =~ /\A[^"].*?@.*?[^"]\Z/)
@local = "\"#{local.join}\""
else
@local = local
end
@domain = domain
@name = nil
@routes = []
end
# Provides the name or 'phrase' of the email address.
#
# For Example:
#
# email = TMail::Address.parse("Mikel Lindsaar <[email protected]>")
# email.name
# #=> "Mikel Lindsaar"
def name
@name
end
# Setter method for the name or phrase of the email
#
# For Example:
#
# email = TMail::Address.parse("[email protected]")
# email.name
# #=> nil
# email.name = "Mikel Lindsaar"
# email.to_s
# #=> "Mikel Lindsaar <[email protected]>"
def name=( str )
@name = str
@name = nil if str and str.empty?
end
#:stopdoc:
alias phrase name
alias phrase= name=
#:startdoc:
# This is still here from RFC 822, and is now obsolete per RFC2822 Section 4.
#
# "When interpreting addresses, the route portion SHOULD be ignored."
#
# It is still here, so you can access it.
#
# Routes return the route portion at the front of the email address, if any.
#
# For Example:
# email = TMail::Address.parse( "<@sa,@another:[email protected]>")
# => #<TMail::Address [email protected]>
# email.to_s
# => "<@sa,@another:[email protected]>"
# email.routes
# => ["sa", "another"]
def routes
@routes
end
def inspect #:nodoc:
"#<#{self.class} #{address()}>"
end
# Returns the local part of the email address
#
# For Example:
#
# email = TMail::Address.parse("[email protected]")
# email.local
# #=> "mikel"
def local
return nil unless @local
return '""' if @local.size == 1 and @local[0].empty?
# Check to see if it is an array before trying to map it
if @local.respond_to?(:map)
@local.map {|i| quote_atom(i) }.join('.')
else
quote_atom(@local)
end
end
# Returns the domain part of the email address
#
# For Example:
#
# email = TMail::Address.parse("[email protected]")
# email.local
# #=> "lindsaar.net"
def domain
return nil unless @domain
join_domain(@domain)
end
# Returns the full specific address itself
#
# For Example:
#
# email = TMail::Address.parse("[email protected]")
# email.address
# #=> "[email protected]"
def spec
s = self.local
d = self.domain
if s and d
s + '@' + d
else
s
end
end
alias address spec
# Provides == function to the email. Only checks the actual address
# and ignores the name/phrase component
#
# For Example
#
# addr1 = TMail::Address.parse("My Address <[email protected]>")
# #=> "#<TMail::Address [email protected]>"
# addr2 = TMail::Address.parse("Another <[email protected]>")
# #=> "#<TMail::Address [email protected]>"
# addr1 == addr2
# #=> true
def ==( other )
other.respond_to? :spec and self.spec == other.spec
end
alias eql? ==
# Provides a unique hash value for this record against the local and domain
# parts, ignores the name/phrase value
#
# email = TMail::Address.parse("[email protected]")
# email.hash
# #=> 18767598
def hash
@local.hash ^ @domain.hash
end
# Duplicates a TMail::Address object returning the duplicate
#
# addr1 = TMail::Address.parse("[email protected]")
# addr2 = addr1.dup
# addr1.id == addr2.id
# #=> false
def dup
obj = self.class.new(@local.dup, @domain.dup)
obj.name = @name.dup if @name
obj.routes.replace @routes
obj
end
include StrategyInterface #:nodoc:
def accept( strategy, dummy1 = nil, dummy2 = nil ) #:nodoc:
unless @local
strategy.meta '<>' # empty return-path
return
end
spec_p = (not @name and @routes.empty?)
if @name
strategy.phrase @name
strategy.space
end
tmp = spec_p ? '' : '<'
unless @routes.empty?
tmp << @routes.map {|i| '@' + i }.join(',') << ':'
end
tmp << self.spec
tmp << '>' unless spec_p
strategy.meta tmp
strategy.lwsp ''
end
end
class AddressGroup
include Enumerable
def address_group?
true
end
def initialize( name, addrs )
@name = name
@addresses = addrs
end
attr_reader :name
def ==( other )
other.respond_to? :to_a and @addresses == other.to_a
end
alias eql? ==
def hash
map {|i| i.hash }.hash
end
def []( idx )
@addresses[idx]
end
def size
@addresses.size
end
def empty?
@addresses.empty?
end
def each( &block )
@addresses.each(&block)
end
def to_a
@addresses.dup
end
alias to_ary to_a
def include?( a )
@addresses.include? a
end
def flatten
set = []
@addresses.each do |a|
if a.respond_to? :flatten
set.concat a.flatten
else
set.push a
end
end
set
end
def each_address( &block )
flatten.each(&block)
end
def add( a )
@addresses.push a
end
alias push add
def delete( a )
@addresses.delete a
end
include StrategyInterface
def accept( strategy, dummy1 = nil, dummy2 = nil )
strategy.phrase @name
strategy.meta ':'
strategy.space
first = true
each do |mbox|
if first
first = false
else
strategy.meta ','
end
strategy.space
mbox.accept strategy
end
strategy.meta ';'
strategy.lwsp ''
end
end
end # module TMail
=begin rdoc
= Attachment handling file
=end
require 'stringio'
module TMail
class Attachment < StringIO
attr_accessor :original_filename, :content_type
end
class Mail
def has_attachments?
multipart? && parts.any? { |part| attachment?(part) }
end
def attachment?(part)
part.disposition_is_attachment? || part.content_type_is_text?
end
def attachments
if multipart?
parts.collect { |part|
if part.multipart?
part.attachments
elsif attachment?(part)
content = part.body # unquoted automatically by TMail#body
file_name = (part['content-location'] &&
part['content-location'].body) ||
part.sub_header("content-type", "name") ||
part.sub_header("content-disposition", "filename")
next if file_name.blank? || content.blank?
attachment = Attachment.new(content)
attachment.original_filename = file_name.strip
attachment.content_type = part.content_type
attachment
end
}.flatten.compact
end
end
end
end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
#:stopdoc:
module TMail
module Base64
module_function
def folding_encode( str, eol = "\n", limit = 60 )
[str].pack('m')
end
def encode( str )
[str].pack('m').tr( "\r\n", '' )
end
def decode( str, strict = false )
str.unpack('m').first
end
end
end
#:startdoc:
#:stopdoc:
unless Enumerable.method_defined?(:map)
module Enumerable #:nodoc:
alias map collect
end
end
unless Enumerable.method_defined?(:select)
module Enumerable #:nodoc:
alias select find_all
end
end
unless Enumerable.method_defined?(:reject)
module Enumerable #:nodoc:
def reject
result = []
each do |i|
result.push i unless yield(i)
end
result
end
end
end
unless Enumerable.method_defined?(:sort_by)
module Enumerable #:nodoc:
def sort_by
map {|i| [yield(i), i] }.sort.map {|val, i| i }
end
end
end
unless File.respond_to?(:read)
def File.read(fname) #:nodoc:
File.open(fname) {|f|
return f.read
}
end
end
#:startdoc:#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
#:stopdoc:
module TMail
class Config
def initialize( strict )
@strict_parse = strict
@strict_base64decode = strict
end
def strict_parse?
@strict_parse
end
attr_writer :strict_parse
def strict_base64decode?
@strict_base64decode
end
attr_writer :strict_base64decode
def new_body_port( mail )
StringPort.new
end
alias new_preamble_port new_body_port
alias new_part_port new_body_port
end
DEFAULT_CONFIG = Config.new(false)
DEFAULT_STRICT_CONFIG = Config.new(true)
def Config.to_config( arg )
return DEFAULT_STRICT_CONFIG if arg == true
return DEFAULT_CONFIG if arg == false
arg or DEFAULT_CONFIG
end
end
#:startdoc:#:stopdoc:
unless Object.respond_to?(:blank?)
class Object
# Check first to see if we are in a Rails environment, no need to
# define these methods if we are
# An object is blank if it's nil, empty, or a whitespace string.
# For example, "", " ", nil, [], and {} are blank.
#
# This simplifies
# if !address.nil? && !address.empty?
# to
# if !address.blank?
def blank?
if respond_to?(:empty?) && respond_to?(:strip)
empty? or strip.empty?
elsif respond_to?(:empty?)
empty?
else
!self
end
end
end
class NilClass
def blank?
true
end
end
class FalseClass
def blank?
true
end
end
class TrueClass
def blank?
false
end
end
class Array
alias_method :blank?, :empty?
end
class Hash
alias_method :blank?, :empty?
end
class String
def blank?
empty? || strip.empty?
end
end
class Numeric
def blank?
false
end
end
end
#:startdoc:#--
# = COPYRIGHT:
#
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
#:stopdoc:
require 'nkf'
require 'tmail/base64'
require 'tmail/stringio'
require 'tmail/utils'
#:startdoc:
module TMail
#:stopdoc:
class << self
attr_accessor :KCODE
end
self.KCODE = 'NONE'
module StrategyInterface
def create_dest( obj )
case obj
when nil
StringOutput.new
when String
StringOutput.new(obj)
when IO, StringOutput
obj
else
raise TypeError, 'cannot handle this type of object for dest'
end
end
module_function :create_dest
#:startdoc:
# Returns the TMail object encoded and ready to be sent via SMTP etc.
# You should call this before you are packaging up your email to
# correctly escape all the values that need escaping in the email, line
# wrap the email etc.
#
# It is also a good idea to call this before you marshal or serialize
# a TMail object.
#
# For Example:
#
# email = TMail::Load(my_email_file)
# email_to_send = email.encoded
def encoded( eol = "\r\n", charset = 'j', dest = nil )
accept_strategy Encoder, eol, charset, dest
end
# Returns the TMail object decoded and ready to be used by you, your
# program etc.
#
# You should call this before you are packaging up your email to
# correctly escape all the values that need escaping in the email, line
# wrap the email etc.
#
# For Example:
#
# email = TMail::Load(my_email_file)
# email_to_send = email.encoded
def decoded( eol = "\n", charset = 'e', dest = nil )
# Turn the E-Mail into a string and return it with all
# encoded characters decoded. alias for to_s
accept_strategy Decoder, eol, charset, dest
end
alias to_s decoded
def accept_strategy( klass, eol, charset, dest = nil ) #:nodoc:
dest ||= ''
accept klass.new( create_dest(dest), charset, eol )
dest
end
end
#:stopdoc:
###
### MIME B encoding decoder
###
class Decoder
include TextUtils
encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?='
ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i
OUTPUT_ENCODING = {
'EUC' => 'e',
'SJIS' => 's',
}
def self.decode( str, encoding = nil )
encoding ||= (OUTPUT_ENCODING[TMail.KCODE] || 'j')
opt = '-mS' + encoding
str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
end
def initialize( dest, encoding = nil, eol = "\n" )
@f = StrategyInterface.create_dest(dest)
@encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
@eol = eol
end
def decode( str )
self.class.decode(str, @encoding)
end
private :decode
def terminate
end
def header_line( str )
@f << decode(str)
end
def header_name( nm )
@f << nm << ': '
end
def header_body( str )
@f << decode(str)
end
def space
@f << ' '
end
alias spc space
def lwsp( str )
@f << str
end
def meta( str )
@f << str
end
def text( str )
@f << decode(str)
end
def phrase( str )
@f << quote_phrase(decode(str))
end
def kv_pair( k, v )
v = dquote(v) unless token_safe?(v)
@f << k << '=' << v
end
def puts( str = nil )
@f << str if str
@f << @eol
end
def write( str )
@f << str
end
end
###
### MIME B-encoding encoder
###
#
# FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
#
class Encoder
include TextUtils
BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
def Encoder.encode( str )
e = new()
e.header_body str
e.terminate
e.dest.string
end
SPACER = "\t"
MAX_LINE_LEN = 78
RFC_2822_MAX_LENGTH = 998
OPTIONS = {
'EUC' => '-Ej -m0',
'SJIS' => '-Sj -m0',
'UTF8' => nil, # FIXME
'NONE' => nil
}
def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil )
@f = StrategyInterface.create_dest(dest)
@opt = OPTIONS[TMail.KCODE]
@eol = eol
@folded = false
@preserve_quotes = true
reset
end
def preserve_quotes=( bool )
@preserve_quotes
end
def preserve_quotes
@preserve_quotes
end
def normalize_encoding( str )
if @opt
then NKF.nkf(@opt, str)
else str
end
end
def reset
@text = ''
@lwsp = ''
@curlen = 0
end
def terminate
add_lwsp ''
reset
end
def dest
@f
end
def puts( str = nil )
@f << str if str
@f << @eol
end
def write( str )
@f << str
end
#
# add
#
def header_line( line )
scanadd line
end
def header_name( name )
add_text name.split(/-/).map {|i| i.capitalize }.join('-')
add_text ':'
add_lwsp ' '
end
def header_body( str )
scanadd normalize_encoding(str)
end
def space
add_lwsp ' '
end
alias spc space
def lwsp( str )
add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
end
def meta( str )
add_text str
end
def text( str )
scanadd normalize_encoding(str)
end
def phrase( str )
str = normalize_encoding(str)
if CONTROL_CHAR === str
scanadd str
else
add_text quote_phrase(str)
end
end
# FIXME: implement line folding
#
def kv_pair( k, v )
return if v.nil?
v = normalize_encoding(v)
if token_safe?(v)
add_text k + '=' + v
elsif not CONTROL_CHAR === v
add_text k + '=' + quote_token(v)
else
# apply RFC2231 encoding
kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
add_text kv
end
end
def encode_value( str )
str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] }
end
private
def scanadd( str, force = false )
types = ''
strs = []
if str.respond_to?(:encoding)
enc = str.encoding
str.force_encoding(Encoding::ASCII_8BIT)
end
until str.empty?
if m = /\A[^\e\t\r\n ]+/.match(str)
types << (force ? 'j' : 'a')
if str.respond_to?(:encoding)
strs.push m[0].force_encoding(enc)
else
strs.push m[0]
end
elsif m = /\A[\t\r\n ]+/.match(str)
types << 's'
if str.respond_to?(:encoding)
strs.push m[0].force_encoding(enc)
else
strs.push m[0]
end
elsif m = /\A\e../.match(str)
esc = m[0]
str = m.post_match
if esc != "\e(B" and m = /\A[^\e]+/.match(str)
types << 'j'
if str.respond_to?(:encoding)
strs.push m[0].force_encoding(enc)
else
strs.push m[0]
end
end
else
raise 'TMail FATAL: encoder scan fail'
end
(str = m.post_match) unless m.nil?
end
do_encode types, strs
end
def do_encode( types, strs )
#
# result : (A|E)(S(A|E))*
# E : W(SW)*
# W : (J|A)+ but must contain J # (J|A)*J(J|A)*
# A : <<A character string not to be encoded>>
# J : <<A character string to be encoded>>
# S : <<LWSP>>
#
# An encoding unit is `E'.
# Input (parameter `types') is (J|A)(J|A|S)*(J|A)
#
if BENCODE_DEBUG
puts
puts '-- do_encode ------------'
puts types.split(//).join(' ')
p strs
end
e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
while m = e.match(types)
pre = m.pre_match
concat_A_S pre, strs[0, pre.size] unless pre.empty?
concat_E m[0], strs[m.begin(0) ... m.end(0)]
types = m.post_match
strs.slice! 0, m.end(0)
end
concat_A_S types, strs
end
def concat_A_S( types, strs )
if RUBY_VERSION < '1.9'
a = ?a; s = ?s
else
a = 'a'.ord; s = 's'.ord
end
i = 0
types.each_byte do |t|
case t
when a then add_text strs[i]
when s then add_lwsp strs[i]
else
raise "TMail FATAL: unknown flag: #{t.chr}"
end
i += 1
end
end
METHOD_ID = {
?j => :extract_J,
?e => :extract_E,
?a => :extract_A,
?s => :extract_S
}
def concat_E( types, strs )
if BENCODE_DEBUG
puts '---- concat_E'
puts "types=#{types.split(//).join(' ')}"
puts "strs =#{strs.inspect}"
end
flush() unless @text.empty?
chunk = ''
strs.each_with_index do |s,i|
mid = METHOD_ID[types[i]]
until s.empty?
unless c = __send__(mid, chunk.size, s)
add_with_encode chunk unless chunk.empty?
flush
chunk = ''
fold
c = __send__(mid, 0, s)
raise 'TMail FATAL: extract fail' unless c
end
chunk << c
end
end
add_with_encode chunk unless chunk.empty?
end
def extract_J( chunksize, str )
size = max_bytes(chunksize, str.size) - 6
size = (size % 2 == 0) ? (size) : (size - 1)
return nil if size <= 0
if str.respond_to?(:encoding)
enc = str.encoding
str.force_encoding(Encoding::ASCII_8BIT)
"\e$B#{str.slice!(0, size)}\e(B".force_encoding(enc)
else
"\e$B#{str.slice!(0, size)}\e(B"
end
end
def extract_A( chunksize, str )
size = max_bytes(chunksize, str.size)
return nil if size <= 0
str.slice!(0, size)
end
alias extract_S extract_A
def max_bytes( chunksize, ssize )
(restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
end
#
# free length buffer
#
def add_text( str )
@text << str
# puts '---- text -------------------------------------'
# puts "+ #{str.inspect}"
# puts "txt >>>#{@text.inspect}<<<"
end
def add_with_encode( str )
@text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
end
def add_lwsp( lwsp )
# puts '---- lwsp -------------------------------------'
# puts "+ #{lwsp.inspect}"
fold if restsize() <= 0
flush(@folded)
@lwsp = lwsp
end
def flush(folded = false)
# puts '---- flush ----'
# puts "spc >>>#{@lwsp.inspect}<<<"
# puts "txt >>>#{@text.inspect}<<<"
@f << @lwsp << @text
if folded
@curlen = 0
else
@curlen += (@lwsp.size + @text.size)
end
@text = ''
@lwsp = ''
end
def fold
# puts '---- fold ----'
unless @f.string =~ /^.*?:$/
@f << @eol
@lwsp = SPACER
else
fold_header
@folded = true
end
@curlen = 0
end
def fold_header
# Called because line is too long - so we need to wrap.
# First look for whitespace in the text
# if it has text, fold there
# check the remaining text, if too long, fold again
# if it doesn't, then don't fold unless the line goes beyond 998 chars
# Check the text to see if there is whitespace, or if not
@wrapped_text = []
until @text.blank?
fold_the_string
end
@text = @wrapped_text.join("#{@eol}#{SPACER}")
end
def fold_the_string
whitespace_location = @text =~ /\s/ || @text.length
# Is the location of the whitespace shorter than the RCF_2822_MAX_LENGTH?
# if there is no whitespace in the string, then this
unless mazsize(whitespace_location) <= 0
@text.strip!
@wrapped_text << @text.slice!(0...whitespace_location)
# If it is not less, we have to wrap it destructively
else
slice_point = RFC_2822_MAX_LENGTH - @curlen - @lwsp.length
@text.strip!
@wrapped_text << @text.slice!(0...slice_point)
end
end
def restsize
MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
end
def mazsize(whitespace_location)
# Per RFC2822, the maximum length of a line is 998 chars
RFC_2822_MAX_LENGTH - (@curlen + @lwsp.size + whitespace_location)
end
end
#:startdoc:
end # module TMail
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/encode'
require 'tmail/address'
require 'tmail/parser'
require 'tmail/config'
require 'tmail/utils'
#:startdoc:
module TMail
# Provides methods to handle and manipulate headers in the email
class HeaderField
include TextUtils
class << self
alias newobj new
def new( name, body, conf = DEFAULT_CONFIG )
klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader
klass.newobj body, conf
end
# Returns a HeaderField object matching the header you specify in the "name" param.
# Requires an initialized TMail::Port to be passed in.
#
# The method searches the header of the Port you pass into it to find a match on
# the header line you pass. Once a match is found, it will unwrap the matching line
# as needed to return an initialized HeaderField object.
#
# If you want to get the Envelope sender of the email object, pass in "EnvelopeSender",
# if you want the From address of the email itself, pass in 'From'.
#
# This is because a mailbox doesn't have the : after the From that designates the
# beginning of the envelope sender (which can be different to the from address of
# the email)
#
# Other fields can be passed as normal, "Reply-To", "Received" etc.
#
# Note: Change of behaviour in 1.2.1 => returns nil if it does not find the specified
# header field, otherwise returns an instantiated object of the correct header class
#
# For example:
# port = TMail::FilePort.new("/test/fixtures/raw_email_simple")
# h = TMail::HeaderField.new_from_port(port, "From")
# h.addrs.to_s #=> "Mikel Lindsaar <[email protected]>"
# h = TMail::HeaderField.new_from_port(port, "EvelopeSender")
# h.addrs.to_s #=> "[email protected]"
# h = TMail::HeaderField.new_from_port(port, "SomeWeirdHeaderField")
# h #=> nil
def new_from_port( port, name, conf = DEFAULT_CONFIG )
if name == "EnvelopeSender"
name = "From"
re = Regexp.new('\A(From) ', 'i')
else
re = Regexp.new('\A(' + Regexp.quote(name) + '):', 'i')
end
str = nil
port.ropen {|f|
f.each do |line|
if m = re.match(line) then str = m.post_match.strip
elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
elsif /\A-*\s*\z/ === line then break
elsif str then break
end
end
}
new(name, str, Config.to_config(conf)) if str
end
def internal_new( name, conf )
FNAME_TO_CLASS[name].newobj('', conf, true)
end
end # class << self
def initialize( body, conf, intern = false )
@body = body
@config = conf
@illegal = false
@parsed = false
if intern
@parsed = true
parse_init
end
end
def inspect
"#<#{self.class} #{@body.inspect}>"
end
def illegal?
@illegal
end
def empty?
ensure_parsed
return true if @illegal
isempty?
end
private
def ensure_parsed
return if @parsed
@parsed = true
parse
end
# defabstract parse
# end
def clear_parse_status
@parsed = false
@illegal = false
end
public
def body
ensure_parsed
v = Decoder.new(s = '')
do_accept v
v.terminate
s
end
def body=( str )
@body = str
clear_parse_status
end
include StrategyInterface
def accept( strategy )
ensure_parsed
do_accept strategy
strategy.terminate
end
# abstract do_accept
end
class UnstructuredHeader < HeaderField
def body
ensure_parsed
@body
end
def body=( arg )
ensure_parsed
@body = arg
end
private
def parse_init
end
def parse
@body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
end
def isempty?
not @body
end
def do_accept( strategy )
strategy.text @body
end
end
class StructuredHeader < HeaderField
def comments
ensure_parsed
if @comments[0]
[Decoder.decode(@comments[0])]
else
@comments
end
end
private
def parse
save = nil
begin
parse_init
do_parse
rescue SyntaxError
if not save and mime_encoded? @body
save = @body
@body = Decoder.decode(save)
retry
elsif save
@body = save
end
@illegal = true
raise if @config.strict_parse?
end
end
def parse_init
@comments = []
init
end
def do_parse
quote_boundary
obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
set obj if obj
end
end
class DateTimeHeader < StructuredHeader
PARSE_TYPE = :DATETIME
def date
ensure_parsed
@date
end
def date=( arg )
ensure_parsed
@date = arg
end
private
def init
@date = nil
end
def set( t )
@date = t
end
def isempty?
not @date
end
def do_accept( strategy )
strategy.meta time2str(@date)
end
end
class AddressHeader < StructuredHeader
PARSE_TYPE = :MADDRESS
def addrs
ensure_parsed
@addrs
end
private
def init
@addrs = []
end
def set( a )
@addrs = a
end
def isempty?
@addrs.empty?
end
def do_accept( strategy )
first = true
@addrs.each do |a|
if first
first = false
else
strategy.meta ','
strategy.space
end
a.accept strategy
end
@comments.each do |c|
strategy.space
strategy.meta '('
strategy.text c
strategy.meta ')'
end
end
end
class ReturnPathHeader < AddressHeader
PARSE_TYPE = :RETPATH
def addr
addrs()[0]
end
def spec
a = addr() or return nil
a.spec
end
def routes
a = addr() or return nil
a.routes
end
private
def do_accept( strategy )
a = addr()
strategy.meta '<'
unless a.routes.empty?
strategy.meta a.routes.map {|i| '@' + i }.join(',')
strategy.meta ':'
end
spec = a.spec
strategy.meta spec if spec
strategy.meta '>'
end
end
class SingleAddressHeader < AddressHeader
def addr
addrs()[0]
end
private
def do_accept( strategy )
a = addr()
a.accept strategy
@comments.each do |c|
strategy.space
strategy.meta '('
strategy.text c
strategy.meta ')'
end
end
end
class MessageIdHeader < StructuredHeader
def id
ensure_parsed
@id
end
def id=( arg )
ensure_parsed
@id = arg
end
private
def init
@id = nil
end
def isempty?
not @id
end
def do_parse
@id = @body.slice(MESSAGE_ID) or
raise SyntaxError, "wrong Message-ID format: #{@body}"
end
def do_accept( strategy )
strategy.meta @id
end
end
class ReferencesHeader < StructuredHeader
def refs
ensure_parsed
@refs
end
def each_id
self.refs.each do |i|
yield i if MESSAGE_ID === i
end
end
def ids
ensure_parsed
@ids
end
def each_phrase
self.refs.each do |i|
yield i unless MESSAGE_ID === i
end
end
def phrases
ret = []
each_phrase {|i| ret.push i }
ret
end
private
def init
@refs = []
@ids = []
end
def isempty?
@ids.empty?
end
def do_parse
str = @body
while m = MESSAGE_ID.match(str)
pre = m.pre_match.strip
@refs.push pre unless pre.empty?
@refs.push s = m[0]
@ids.push s
str = m.post_match
end
str = str.strip
@refs.push str unless str.empty?
end
def do_accept( strategy )
first = true
@ids.each do |i|
if first
first = false
else
strategy.space
end
strategy.meta i
end
end
end
class ReceivedHeader < StructuredHeader
PARSE_TYPE = :RECEIVED
def from
ensure_parsed
@from
end
def from=( arg )
ensure_parsed
@from = arg
end
def by
ensure_parsed
@by
end
def by=( arg )
ensure_parsed
@by = arg
end
def via
ensure_parsed
@via
end
def via=( arg )
ensure_parsed
@via = arg
end
def with
ensure_parsed
@with
end
def id
ensure_parsed
@id
end
def id=( arg )
ensure_parsed
@id = arg
end
def _for
ensure_parsed
@_for
end
def _for=( arg )
ensure_parsed
@_for = arg
end
def date
ensure_parsed
@date
end
def date=( arg )
ensure_parsed
@date = arg
end
private
def init
@from = @by = @via = @with = @id = @_for = nil
@with = []
@date = nil
end
def set( args )
@from, @by, @via, @with, @id, @_for, @date = *args
end
def isempty?
@with.empty? and not (@from or @by or @via or @id or @_for or @date)
end
def do_accept( strategy )
list = []
list.push 'from ' + @from if @from
list.push 'by ' + @by if @by
list.push 'via ' + @via if @via
@with.each do |i|
list.push 'with ' + i
end
list.push 'id ' + @id if @id
list.push 'for <' + @_for + '>' if @_for
first = true
list.each do |i|
strategy.space unless first
strategy.meta i
first = false
end
if @date
strategy.meta ';'
strategy.space
strategy.meta time2str(@date)
end
end
end
class KeywordsHeader < StructuredHeader
PARSE_TYPE = :KEYWORDS
def keys
ensure_parsed
@keys
end
private
def init
@keys = []
end
def set( a )
@keys = a
end
def isempty?
@keys.empty?
end
def do_accept( strategy )
first = true
@keys.each do |i|
if first
first = false
else
strategy.meta ','
end
strategy.meta i
end
end
end
class EncryptedHeader < StructuredHeader
PARSE_TYPE = :ENCRYPTED
def encrypter
ensure_parsed
@encrypter
end
def encrypter=( arg )
ensure_parsed
@encrypter = arg
end
def keyword
ensure_parsed
@keyword
end
def keyword=( arg )
ensure_parsed
@keyword = arg
end
private
def init
@encrypter = nil
@keyword = nil
end
def set( args )
@encrypter, @keyword = args
end
def isempty?
not (@encrypter or @keyword)
end
def do_accept( strategy )
if @key
strategy.meta @encrypter + ','
strategy.space
strategy.meta @keyword
else
strategy.meta @encrypter
end
end
end
class MimeVersionHeader < StructuredHeader
PARSE_TYPE = :MIMEVERSION
def major
ensure_parsed
@major
end
def major=( arg )
ensure_parsed
@major = arg
end
def minor
ensure_parsed
@minor
end
def minor=( arg )
ensure_parsed
@minor = arg
end
def version
sprintf('%d.%d', major, minor)
end
private
def init
@major = nil
@minor = nil
end
def set( args )
@major, @minor = *args
end
def isempty?
not (@major or @minor)
end
def do_accept( strategy )
strategy.meta sprintf('%d.%d', @major, @minor)
end
end
class ContentTypeHeader < StructuredHeader
PARSE_TYPE = :CTYPE
def main_type
ensure_parsed
@main
end
def main_type=( arg )
ensure_parsed
@main = arg.downcase
end
def sub_type
ensure_parsed
@sub
end
def sub_type=( arg )
ensure_parsed
@sub = arg.downcase
end
def content_type
ensure_parsed
@sub ? sprintf('%s/%s', @main, @sub) : @main
end
def params
ensure_parsed
unless @params.blank?
@params.each do |k, v|
@params[k] = unquote(v)
end
end
@params
end
def []( key )
ensure_parsed
@params and unquote(@params[key])
end
def []=( key, val )
ensure_parsed
(@params ||= {})[key] = val
end
private
def init
@main = @sub = @params = nil
end
def set( args )
@main, @sub, @params = *args
end
def isempty?
not (@main or @sub)
end
def do_accept( strategy )
if @sub
strategy.meta sprintf('%s/%s', @main, @sub)
else
strategy.meta @main
end
@params.each do |k,v|
if v
strategy.meta ';'
strategy.space
strategy.kv_pair k, v
end
end
end
end
class ContentTransferEncodingHeader < StructuredHeader
PARSE_TYPE = :CENCODING
def encoding
ensure_parsed
@encoding
end
def encoding=( arg )
ensure_parsed
@encoding = arg
end
private
def init
@encoding = nil
end
def set( s )
@encoding = s
end
def isempty?
not @encoding
end
def do_accept( strategy )
strategy.meta @encoding.capitalize
end
end
class ContentDispositionHeader < StructuredHeader
PARSE_TYPE = :CDISPOSITION
def disposition
ensure_parsed
@disposition
end
def disposition=( str )
ensure_parsed
@disposition = str.downcase
end
def params
ensure_parsed
unless @params.blank?
@params.each do |k, v|
@params[k] = unquote(v)
end
end
@params
end
def []( key )
ensure_parsed
@params and unquote(@params[key])
end
def []=( key, val )
ensure_parsed
(@params ||= {})[key] = val
end
private
def init
@disposition = @params = nil
end
def set( args )
@disposition, @params = *args
end
def isempty?
not @disposition and (not @params or @params.empty?)
end
def do_accept( strategy )
strategy.meta @disposition
@params.each do |k,v|
strategy.meta ';'
strategy.space
strategy.kv_pair k, unquote(v)
end
end
end
class HeaderField # redefine
FNAME_TO_CLASS = {
'date' => DateTimeHeader,
'resent-date' => DateTimeHeader,
'to' => AddressHeader,
'cc' => AddressHeader,
'bcc' => AddressHeader,
'from' => AddressHeader,
'reply-to' => AddressHeader,
'resent-to' => AddressHeader,
'resent-cc' => AddressHeader,
'resent-bcc' => AddressHeader,
'resent-from' => AddressHeader,
'resent-reply-to' => AddressHeader,
'sender' => SingleAddressHeader,
'resent-sender' => SingleAddressHeader,
'return-path' => ReturnPathHeader,
'message-id' => MessageIdHeader,
'resent-message-id' => MessageIdHeader,
'in-reply-to' => ReferencesHeader,
'received' => ReceivedHeader,
'references' => ReferencesHeader,
'keywords' => KeywordsHeader,
'encrypted' => EncryptedHeader,
'mime-version' => MimeVersionHeader,
'content-type' => ContentTypeHeader,
'content-transfer-encoding' => ContentTransferEncodingHeader,
'content-disposition' => ContentDispositionHeader,
'content-id' => MessageIdHeader,
'subject' => UnstructuredHeader,
'comments' => UnstructuredHeader,
'content-description' => UnstructuredHeader
}
end
end # module TMail
#:stopdoc:
# This is here for Rolls.
# Rolls uses this instead of lib/tmail.rb.
require 'tmail/version'
require 'tmail/mail'
require 'tmail/mailbox'
require 'tmail/core_extensions'
#:startdoc:=begin rdoc
= interface.rb Provides an interface to the TMail object
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
# TMail::Mail objects get accessed primarily through the methods in this file.
#
#
require 'tmail/utils'
module TMail
class Mail
# Allows you to query the mail object with a string to get the contents
# of the field you want.
#
# Returns a string of the exact contents of the field
#
# mail.from = "mikel <[email protected]>"
# mail.header_string("From") #=> "mikel <[email protected]>"
def header_string( name, default = nil )
h = @header[name.downcase] or return default
h.to_s
end
#:stopdoc:
#--
#== Attributes
include TextUtils
def set_string_array_attr( key, strs )
strs.flatten!
if strs.empty?
@header.delete key.downcase
else
store key, strs.join(', ')
end
strs
end
private :set_string_array_attr
def set_string_attr( key, str )
if str
store key, str
else
@header.delete key.downcase
end
str
end
private :set_string_attr
def set_addrfield( name, arg )
if arg
h = HeaderField.internal_new(name, @config)
h.addrs.replace [arg].flatten
@header[name] = h
else
@header.delete name
end
arg
end
private :set_addrfield
def addrs2specs( addrs )
return nil unless addrs
list = addrs.map {|addr|
if addr.address_group?
then addr.map {|a| a.spec }
else addr.spec
end
}.flatten
return nil if list.empty?
list
end
private :addrs2specs
#:startdoc:
#== Date and Time methods
# Returns the date of the email message as per the "date" header value or returns
# nil by default (if no date field exists).
#
# You can also pass whatever default you want into this method and it will return
# that instead of nil if there is no date already set.
def date( default = nil )
if h = @header['date']
h.date
else
default
end
end
# Destructively sets the date of the mail object with the passed Time instance,
# returns a Time instance set to the date/time of the mail
#
# Example:
#
# now = Time.now
# mail.date = now
# mail.date #=> Sat Nov 03 18:47:50 +1100 2007
# mail.date.class #=> Time
def date=( time )
if time
store 'Date', time2str(time)
else
@header.delete 'date'
end
time
end
# Returns the time of the mail message formatted to your taste using a
# strftime format string. If no date set returns nil by default or whatever value
# you pass as the second optional parameter.
#
# time = Time.now # (on Nov 16 2007)
# mail.date = time
# mail.strftime("%D") #=> "11/16/07"
def strftime( fmt, default = nil )
if t = date
t.strftime(fmt)
else
default
end
end
#== Destination methods
# Return a TMail::Addresses instance for each entry in the "To:" field of the mail object header.
#
# If the "To:" field does not exist, will return nil by default or the value you
# pass as the optional parameter.
#
# Example:
#
# mail = TMail::Mail.new
# mail.to_addrs #=> nil
# mail.to_addrs([]) #=> []
# mail.to = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.to_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def to_addrs( default = nil )
if h = @header['to']
h.addrs
else
default
end
end
# Return a TMail::Addresses instance for each entry in the "Cc:" field of the mail object header.
#
# If the "Cc:" field does not exist, will return nil by default or the value you
# pass as the optional parameter.
#
# Example:
#
# mail = TMail::Mail.new
# mail.cc_addrs #=> nil
# mail.cc_addrs([]) #=> []
# mail.cc = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.cc_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def cc_addrs( default = nil )
if h = @header['cc']
h.addrs
else
default
end
end
# Return a TMail::Addresses instance for each entry in the "Bcc:" field of the mail object header.
#
# If the "Bcc:" field does not exist, will return nil by default or the value you
# pass as the optional parameter.
#
# Example:
#
# mail = TMail::Mail.new
# mail.bcc_addrs #=> nil
# mail.bcc_addrs([]) #=> []
# mail.bcc = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.bcc_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def bcc_addrs( default = nil )
if h = @header['bcc']
h.addrs
else
default
end
end
# Destructively set the to field of the "To:" header to equal the passed in string.
#
# TMail will parse your contents and turn each valid email address into a TMail::Address
# object before assigning it to the mail message.
#
# Example:
#
# mail = TMail::Mail.new
# mail.to = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.to_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def to_addrs=( arg )
set_addrfield 'to', arg
end
# Destructively set the to field of the "Cc:" header to equal the passed in string.
#
# TMail will parse your contents and turn each valid email address into a TMail::Address
# object before assigning it to the mail message.
#
# Example:
#
# mail = TMail::Mail.new
# mail.cc = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.cc_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def cc_addrs=( arg )
set_addrfield 'cc', arg
end
# Destructively set the to field of the "Bcc:" header to equal the passed in string.
#
# TMail will parse your contents and turn each valid email address into a TMail::Address
# object before assigning it to the mail message.
#
# Example:
#
# mail = TMail::Mail.new
# mail.bcc = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.bcc_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def bcc_addrs=( arg )
set_addrfield 'bcc', arg
end
# Returns who the email is to as an Array of email addresses as opposed to an Array of
# TMail::Address objects which is what Mail#to_addrs returns
#
# Example:
#
# mail = TMail::Mail.new
# mail.to = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.to #=> ["[email protected]", "[email protected]"]
def to( default = nil )
addrs2specs(to_addrs(nil)) || default
end
# Returns who the email cc'd as an Array of email addresses as opposed to an Array of
# TMail::Address objects which is what Mail#to_addrs returns
#
# Example:
#
# mail = TMail::Mail.new
# mail.cc = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.cc #=> ["[email protected]", "[email protected]"]
def cc( default = nil )
addrs2specs(cc_addrs(nil)) || default
end
# Returns who the email bcc'd as an Array of email addresses as opposed to an Array of
# TMail::Address objects which is what Mail#to_addrs returns
#
# Example:
#
# mail = TMail::Mail.new
# mail.bcc = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.bcc #=> ["[email protected]", "[email protected]"]
def bcc( default = nil )
addrs2specs(bcc_addrs(nil)) || default
end
# Destructively sets the "To:" field to the passed array of strings (which should be valid
# email addresses)
#
# Example:
#
# mail = TMail::Mail.new
# mail.to = ["[email protected]", "Mikel <[email protected]>"]
# mail.to #=> ["[email protected]", "[email protected]"]
# mail['to'].to_s #=> "[email protected], Mikel <[email protected]>"
def to=( *strs )
set_string_array_attr 'To', strs
end
# Destructively sets the "Cc:" field to the passed array of strings (which should be valid
# email addresses)
#
# Example:
#
# mail = TMail::Mail.new
# mail.cc = ["[email protected]", "Mikel <[email protected]>"]
# mail.cc #=> ["[email protected]", "[email protected]"]
# mail['cc'].to_s #=> "[email protected], Mikel <[email protected]>"
def cc=( *strs )
set_string_array_attr 'Cc', strs
end
# Destructively sets the "Bcc:" field to the passed array of strings (which should be valid
# email addresses)
#
# Example:
#
# mail = TMail::Mail.new
# mail.bcc = ["[email protected]", "Mikel <[email protected]>"]
# mail.bcc #=> ["[email protected]", "[email protected]"]
# mail['bcc'].to_s #=> "[email protected], Mikel <[email protected]>"
def bcc=( *strs )
set_string_array_attr 'Bcc', strs
end
#== Originator methods
# Return a TMail::Addresses instance for each entry in the "From:" field of the mail object header.
#
# If the "From:" field does not exist, will return nil by default or the value you
# pass as the optional parameter.
#
# Example:
#
# mail = TMail::Mail.new
# mail.from_addrs #=> nil
# mail.from_addrs([]) #=> []
# mail.from = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.from_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def from_addrs( default = nil )
if h = @header['from']
h.addrs
else
default
end
end
# Destructively set the to value of the "From:" header to equal the passed in string.
#
# TMail will parse your contents and turn each valid email address into a TMail::Address
# object before assigning it to the mail message.
#
# Example:
#
# mail = TMail::Mail.new
# mail.from_addrs = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.from_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def from_addrs=( arg )
set_addrfield 'from', arg
end
# Returns who the email is from as an Array of email address strings instead to an Array of
# TMail::Address objects which is what Mail#from_addrs returns
#
# Example:
#
# mail = TMail::Mail.new
# mail.from = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.from #=> ["[email protected]", "[email protected]"]
def from( default = nil )
addrs2specs(from_addrs(nil)) || default
end
# Destructively sets the "From:" field to the passed array of strings (which should be valid
# email addresses)
#
# Example:
#
# mail = TMail::Mail.new
# mail.from = ["[email protected]", "Mikel <[email protected]>"]
# mail.from #=> ["[email protected]", "[email protected]"]
# mail['from'].to_s #=> "[email protected], Mikel <[email protected]>"
def from=( *strs )
set_string_array_attr 'From', strs
end
# Returns the "friendly" human readable part of the address
#
# Example:
#
# mail = TMail::Mail.new
# mail.from = "Mikel Lindsaar <[email protected]>"
# mail.friendly_from #=> "Mikel Lindsaar"
def friendly_from( default = nil )
h = @header['from']
a, = h.addrs
return default unless a
return a.phrase if a.phrase
return h.comments.join(' ') unless h.comments.empty?
a.spec
end
# Return a TMail::Addresses instance for each entry in the "Reply-To:" field of the mail object header.
#
# If the "Reply-To:" field does not exist, will return nil by default or the value you
# pass as the optional parameter.
#
# Example:
#
# mail = TMail::Mail.new
# mail.reply_to_addrs #=> nil
# mail.reply_to_addrs([]) #=> []
# mail.reply_to = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.reply_to_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def reply_to_addrs( default = nil )
if h = @header['reply-to']
h.addrs.blank? ? default : h.addrs
else
default
end
end
# Destructively set the to value of the "Reply-To:" header to equal the passed in argument.
#
# TMail will parse your contents and turn each valid email address into a TMail::Address
# object before assigning it to the mail message.
#
# Example:
#
# mail = TMail::Mail.new
# mail.reply_to_addrs = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.reply_to_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def reply_to_addrs=( arg )
set_addrfield 'reply-to', arg
end
# Returns who the email is from as an Array of email address strings instead to an Array of
# TMail::Address objects which is what Mail#reply_to_addrs returns
#
# Example:
#
# mail = TMail::Mail.new
# mail.reply_to = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.reply_to #=> ["[email protected]", "[email protected]"]
def reply_to( default = nil )
addrs2specs(reply_to_addrs(nil)) || default
end
# Destructively sets the "Reply-To:" field to the passed array of strings (which should be valid
# email addresses)
#
# Example:
#
# mail = TMail::Mail.new
# mail.reply_to = ["[email protected]", "Mikel <[email protected]>"]
# mail.reply_to #=> ["[email protected]", "[email protected]"]
# mail['reply_to'].to_s #=> "[email protected], Mikel <[email protected]>"
def reply_to=( *strs )
set_string_array_attr 'Reply-To', strs
end
# Return a TMail::Addresses instance of the "Sender:" field of the mail object header.
#
# If the "Sender:" field does not exist, will return nil by default or the value you
# pass as the optional parameter.
#
# Example:
#
# mail = TMail::Mail.new
# mail.sender #=> nil
# mail.sender([]) #=> []
# mail.sender = "Mikel <[email protected]>"
# mail.reply_to_addrs #=> [#<TMail::Address [email protected]>]
def sender_addr( default = nil )
f = @header['sender'] or return default
f.addr or return default
end
# Destructively set the to value of the "Sender:" header to equal the passed in argument.
#
# TMail will parse your contents and turn each valid email address into a TMail::Address
# object before assigning it to the mail message.
#
# Example:
#
# mail = TMail::Mail.new
# mail.sender_addrs = "Mikel <[email protected]>, another Mikel <[email protected]>"
# mail.sender_addrs #=> [#<TMail::Address [email protected]>, #<TMail::Address [email protected]>]
def sender_addr=( addr )
if addr
h = HeaderField.internal_new('sender', @config)
h.addr = addr
@header['sender'] = h
else
@header.delete 'sender'
end
addr
end
# Returns who the sender of this mail is as string instead to an Array of
# TMail::Address objects which is what Mail#sender_addr returns
#
# Example:
#
# mail = TMail::Mail.new
# mail.sender = "Mikel <[email protected]>"
# mail.sender #=> "[email protected]"
def sender( default = nil )
f = @header['sender'] or return default
a = f.addr or return default
a.spec
end
# Destructively sets the "Sender:" field to the passed string (which should be a valid
# email address)
#
# Example:
#
# mail = TMail::Mail.new
# mail.sender = "[email protected]"
# mail.sender #=> "[email protected]"
# mail['sender'].to_s #=> "[email protected]"
def sender=( str )
set_string_attr 'Sender', str
end
#== Subject methods
# Returns the subject of the mail instance.
#
# If the subject field does not exist, returns nil by default or you can pass in as
# the parameter for what you want the default value to be.
#
# Example:
#
# mail = TMail::Mail.new
# mail.subject #=> nil
# mail.subject("") #=> ""
# mail.subject = "Hello"
# mail.subject #=> "Hello"
def subject( default = nil )
if h = @header['subject']
h.body
else
default
end
end
alias quoted_subject subject
# Destructively sets the passed string as the subject of the mail message.
#
# Example
#
# mail = TMail::Mail.new
# mail.subject #=> "This subject"
# mail.subject = "Another subject"
# mail.subject #=> "Another subject"
def subject=( str )
set_string_attr 'Subject', str
end
#== Message Identity & Threading Methods
# Returns the message ID for this mail object instance.
#
# If the message_id field does not exist, returns nil by default or you can pass in as
# the parameter for what you want the default value to be.
#
# Example:
#
# mail = TMail::Mail.new
# mail.message_id #=> nil
# mail.message_id(TMail.new_message_id) #=> "<[email protected]>"
# mail.message_id = TMail.new_message_id
# mail.message_id #=> "<[email protected]>"
def message_id( default = nil )
if h = @header['message-id']
h.id || default
else
default
end
end
# Destructively sets the message ID of the mail object instance to the passed in string
#
# Invalid message IDs are ignored (silently, unless configured otherwise) and result in
# a nil message ID. Left and right angle brackets are required.
#
# Example:
#
# mail = TMail::Mail.new
# mail.message_id = "<[email protected]>"
# mail.message_id #=> "<[email protected]>"
# mail.message_id = "this_is_my_badly_formatted_message_id"
# mail.message_id #=> nil
def message_id=( str )
set_string_attr 'Message-Id', str
end
# Returns the "In-Reply-To:" field contents as an array of this mail instance if it exists
#
# If the in_reply_to field does not exist, returns nil by default or you can pass in as
# the parameter for what you want the default value to be.
#
# Example:
#
# mail = TMail::Mail.new
# mail.in_reply_to #=> nil
# mail.in_reply_to([]) #=> []
# TMail::Mail.load("../test/fixtures/raw_email_reply")
# mail.in_reply_to #=> ["<[email protected]>"]
def in_reply_to( default = nil )
if h = @header['in-reply-to']
h.ids
else
default
end
end
# Destructively sets the value of the "In-Reply-To:" field of an email.
#
# Accepts an array of a single string of a message id
#
# Example:
#
# mail = TMail::Mail.new
# mail.in_reply_to = ["<[email protected]>"]
# mail.in_reply_to #=> ["<[email protected]>"]
def in_reply_to=( *idstrs )
set_string_array_attr 'In-Reply-To', idstrs
end
# Returns the references of this email (prior messages relating to this message)
# as an array of message ID strings. Useful when you are trying to thread an
# email.
#
# If the references field does not exist, returns nil by default or you can pass in as
# the parameter for what you want the default value to be.
#
# Example:
#
# mail = TMail::Mail.new
# mail.references #=> nil
# mail.references([]) #=> []
# mail = TMail::Mail.load("../test/fixtures/raw_email_reply")
# mail.references #=> ["<[email protected]>", "<[email protected]>"]
def references( default = nil )
if h = @header['references']
h.refs
else
default
end
end
# Destructively sets the value of the "References:" field of an email.
#
# Accepts an array of strings of message IDs
#
# Example:
#
# mail = TMail::Mail.new
# mail.references = ["<[email protected]>"]
# mail.references #=> ["<[email protected]>"]
def references=( *strs )
set_string_array_attr 'References', strs
end
#== MIME header methods
# Returns the listed MIME version of this email from the "Mime-Version:" header field
#
# If the mime_version field does not exist, returns nil by default or you can pass in as
# the parameter for what you want the default value to be.
#
# Example:
#
# mail = TMail::Mail.new
# mail.mime_version #=> nil
# mail.mime_version([]) #=> []
# mail = TMail::Mail.load("../test/fixtures/raw_email")
# mail.mime_version #=> "1.0"
def mime_version( default = nil )
if h = @header['mime-version']
h.version || default
else
default
end
end
def mime_version=( m, opt = nil )
if opt
if h = @header['mime-version']
h.major = m
h.minor = opt
else
store 'Mime-Version', "#{m}.#{opt}"
end
else
store 'Mime-Version', m
end
m
end
# Returns the current "Content-Type" of the mail instance.
#
# If the content_type field does not exist, returns nil by default or you can pass in as
# the parameter for what you want the default value to be.
#
# Example:
#
# mail = TMail::Mail.new
# mail.content_type #=> nil
# mail.content_type([]) #=> []
# mail = TMail::Mail.load("../test/fixtures/raw_email")
# mail.content_type #=> "text/plain"
def content_type( default = nil )
if h = @header['content-type']
h.content_type || default
else
default
end
end
# Returns the current main type of the "Content-Type" of the mail instance.
#
# If the content_type field does not exist, returns nil by default or you can pass in as
# the parameter for what you want the default value to be.
#
# Example:
#
# mail = TMail::Mail.new
# mail.main_type #=> nil
# mail.main_type([]) #=> []
# mail = TMail::Mail.load("../test/fixtures/raw_email")
# mail.main_type #=> "text"
def main_type( default = nil )
if h = @header['content-type']
h.main_type || default
else
default
end
end
# Returns the current sub type of the "Content-Type" of the mail instance.
#
# If the content_type field does not exist, returns nil by default or you can pass in as
# the parameter for what you want the default value to be.
#
# Example:
#
# mail = TMail::Mail.new
# mail.sub_type #=> nil
# mail.sub_type([]) #=> []
# mail = TMail::Mail.load("../test/fixtures/raw_email")
# mail.sub_type #=> "plain"
def sub_type( default = nil )
if h = @header['content-type']
h.sub_type || default
else
default
end
end
# Destructively sets the "Content-Type:" header field of this mail object
#
# Allows you to set the main type, sub type as well as parameters to the field.
# The main type and sub type need to be a string.
#
# The optional params hash can be passed with keys as symbols and values as a string,
# or strings as keys and values.
#
# Example:
#
# mail = TMail::Mail.new
# mail.set_content_type("text", "plain")
# mail.to_s #=> "Content-Type: text/plain\n\n"
#
# mail.set_content_type("text", "plain", {:charset => "EUC-KR", :format => "flowed"})
# mail.to_s #=> "Content-Type: text/plain; charset=EUC-KR; format=flowed\n\n"
#
# mail.set_content_type("text", "plain", {"charset" => "EUC-KR", "format" => "flowed"})
# mail.to_s #=> "Content-Type: text/plain; charset=EUC-KR; format=flowed\n\n"
def set_content_type( str, sub = nil, param = nil )
if sub
main, sub = str, sub
else
main, sub = str.split(%r</>, 2)
raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
end
if h = @header['content-type']
h.main_type = main
h.sub_type = sub
h.params.clear
else
store 'Content-Type', "#{main}/#{sub}"
end
@header['content-type'].params.replace param if param
str
end
alias content_type= set_content_type
# Returns the named type parameter as a string, from the "Content-Type:" header.
#
# Example:
#
# mail = TMail::Mail.new
# mail.type_param("charset") #=> nil
# mail.type_param("charset", []) #=> []
# mail.set_content_type("text", "plain", {:charset => "EUC-KR", :format => "flowed"})
# mail.type_param("charset") #=> "EUC-KR"
# mail.type_param("format") #=> "flowed"
def type_param( name, default = nil )
if h = @header['content-type']
h[name] || default
else
default
end
end
# Returns the character set of the email. Returns nil if no encoding set or returns
# whatever default you pass as a parameter - note passing the parameter does NOT change
# the mail object in any way.
#
# Example:
#
# mail = TMail::Mail.load("path_to/utf8_email")
# mail.charset #=> "UTF-8"
#
# mail = TMail::Mail.new
# mail.charset #=> nil
# mail.charset("US-ASCII") #=> "US-ASCII"
def charset( default = nil )
if h = @header['content-type']
h['charset'] or default
else
default
end
end
# Destructively sets the character set used by this mail object to the passed string, you
# should note though that this does nothing to the mail body, just changes the header
# value, you will need to transliterate the body as well to match whatever you put
# in this header value if you are changing character sets.
#
# Example:
#
# mail = TMail::Mail.new
# mail.charset #=> nil
# mail.charset = "UTF-8"
# mail.charset #=> "UTF-8"
def charset=( str )
if str
if h = @header[ 'content-type' ]
h['charset'] = str
else
store 'Content-Type', "text/plain; charset=#{str}"
end
end
str
end
# Returns the transfer encoding of the email. Returns nil if no encoding set or returns
# whatever default you pass as a parameter - note passing the parameter does NOT change
# the mail object in any way.
#
# Example:
#
# mail = TMail::Mail.load("path_to/base64_encoded_email")
# mail.transfer_encoding #=> "base64"
#
# mail = TMail::Mail.new
# mail.transfer_encoding #=> nil
# mail.transfer_encoding("base64") #=> "base64"
def transfer_encoding( default = nil )
if h = @header['content-transfer-encoding']
h.encoding || default
else
default
end
end
# Destructively sets the transfer encoding of the mail object to the passed string, you
# should note though that this does nothing to the mail body, just changes the header
# value, you will need to encode or decode the body as well to match whatever you put
# in this header value.
#
# Example:
#
# mail = TMail::Mail.new
# mail.transfer_encoding #=> nil
# mail.transfer_encoding = "base64"
# mail.transfer_encoding #=> "base64"
def transfer_encoding=( str )
set_string_attr 'Content-Transfer-Encoding', str
end
alias encoding transfer_encoding
alias encoding= transfer_encoding=
alias content_transfer_encoding transfer_encoding
alias content_transfer_encoding= transfer_encoding=
# Returns the content-disposition of the mail object, returns nil or the passed
# default value if given
#
# Example:
#
# mail = TMail::Mail.load("path_to/raw_mail_with_attachment")
# mail.disposition #=> "attachment"
#
# mail = TMail::Mail.load("path_to/plain_simple_email")
# mail.disposition #=> nil
# mail.disposition(false) #=> false
def disposition( default = nil )
if h = @header['content-disposition']
h.disposition || default
else
default
end
end
alias content_disposition disposition
# Allows you to set the content-disposition of the mail object. Accepts a type
# and a hash of parameters.
#
# Example:
#
# mail.set_disposition("attachment", {:filename => "test.rb"})
# mail.disposition #=> "attachment"
# mail['content-disposition'].to_s #=> "attachment; filename=test.rb"
def set_disposition( str, params = nil )
if h = @header['content-disposition']
h.disposition = str
h.params.clear
else
store('Content-Disposition', str)
h = @header['content-disposition']
end
h.params.replace params if params
end
alias disposition= set_disposition
alias set_content_disposition set_disposition
alias content_disposition= set_disposition
# Returns the value of a parameter in an existing content-disposition header
#
# Example:
#
# mail.set_disposition("attachment", {:filename => "test.rb"})
# mail['content-disposition'].to_s #=> "attachment; filename=test.rb"
# mail.disposition_param("filename") #=> "test.rb"
# mail.disposition_param("missing_param_key") #=> nil
# mail.disposition_param("missing_param_key", false) #=> false
# mail.disposition_param("missing_param_key", "Nothing to see here") #=> "Nothing to see here"
def disposition_param( name, default = nil )
if h = @header['content-disposition']
h[name] || default
else
default
end
end
# Convert the Mail object's body into a Base64 encoded email
# returning the modified Mail object
def base64_encode!
store 'Content-Transfer-Encoding', 'Base64'
self.body = base64_encode
end
# Return the result of encoding the TMail::Mail object body
# without altering the current body
def base64_encode
Base64.folding_encode(self.body)
end
# Convert the Mail object's body into a Base64 decoded email
# returning the modified Mail object
def base64_decode!
if /base64/i === self.transfer_encoding('')
store 'Content-Transfer-Encoding', '8bit'
self.body = base64_decode
end
end
# Returns the result of decoding the TMail::Mail object body
# without altering the current body
def base64_decode
Base64.decode(self.body, @config.strict_base64decode?)
end
# Returns an array of each destination in the email message including to: cc: or bcc:
#
# Example:
#
# mail.to = "Mikel <[email protected]>"
# mail.cc = "Trans <[email protected]>"
# mail.bcc = "bob <[email protected]>"
# mail.destinations #=> ["[email protected]", "[email protected]", "[email protected]"]
def destinations( default = nil )
ret = []
%w( to cc bcc ).each do |nm|
if h = @header[nm]
h.addrs.each {|i| ret.push i.address }
end
end
ret.empty? ? default : ret
end
# Yields a block of destination, yielding each as a string.
# (from the destinations example)
# mail.each_destination { |d| puts "#{d.class}: #{d}" }
# String: [email protected]
# String: [email protected]
# String: [email protected]
def each_destination( &block )
destinations([]).each do |i|
if Address === i
yield i
else
i.each(&block)
end
end
end
alias each_dest each_destination
# Returns an array of reply to addresses that the Mail object has,
# or if the Mail message has no reply-to, returns an array of the
# Mail objects from addresses. Else returns the default which can
# either be passed as a parameter or defaults to nil
#
# Example:
# mail.from = "Mikel <[email protected]>"
# mail.reply_to = nil
# mail.reply_addresses #=> [""]
#
def reply_addresses( default = nil )
reply_to_addrs(nil) or from_addrs(nil) or default
end
# Returns the "sender" field as an array -> useful to find out who to
# send an error email to.
def error_reply_addresses( default = nil )
if s = sender(nil)
[s]
else
from_addrs(default)
end
end
# Returns true if the Mail object is a multipart message
def multipart?
main_type('').downcase == 'multipart'
end
# Creates a new email in reply to self. Sets the In-Reply-To and
# References headers for you automagically.
#
# Example:
# mail = TMail::Mail.load("my_email")
# reply_email = mail.create_reply
# reply_email.class #=> TMail::Mail
# reply_email.references #=> ["<[email protected]>"]
# reply_email.in_reply_to #=> ["<[email protected]>"]
def create_reply
setup_reply create_empty_mail()
end
# Creates a new email in reply to self. Sets the In-Reply-To and
# References headers for you automagically.
#
# Example:
# mail = TMail::Mail.load("my_email")
# forward_email = mail.create_forward
# forward_email.class #=> TMail::Mail
# forward_email.content_type #=> "multipart/mixed"
# forward_email.body #=> "Attachment: (unnamed)"
# forward_email.encoded #=> Returns the original email as a MIME attachment
def create_forward
setup_forward create_empty_mail()
end
#:stopdoc:
private
def create_empty_mail
self.class.new(StringPort.new(''), @config)
end
def setup_reply( mail )
if tmp = reply_addresses(nil)
mail.to_addrs = tmp
end
mid = message_id(nil)
tmp = references(nil) || []
tmp.push mid if mid
mail.in_reply_to = [mid] if mid
mail.references = tmp unless tmp.empty?
mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
mail.mime_version = '1.0'
mail
end
def setup_forward( mail )
m = Mail.new(StringPort.new(''))
m.body = decoded
m.set_content_type 'message', 'rfc822'
m.encoding = encoding('7bit')
mail.parts.push m
# call encoded to reparse the message
mail.encoded
mail
end
#:startdoc:
end # class Mail
end # module TMail
#:stopdoc:
require 'tmail/mailbox'
#:startdoc:=begin rdoc
= Mail class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/interface'
require 'tmail/encode'
require 'tmail/header'
require 'tmail/port'
require 'tmail/config'
require 'tmail/utils'
require 'tmail/attachments'
require 'tmail/quoting'
require 'socket'
module TMail
# == Mail Class
#
# Accessing a TMail object done via the TMail::Mail class. As email can be fairly complex
# creatures, you will find a large amount of accessor and setter methods in this class!
#
# Most of the below methods handle the header, in fact, what TMail does best is handle the
# header of the email object. There are only a few methods that deal directly with the body
# of the email, such as base64_encode and base64_decode.
#
# === Using TMail inside your code
#
# The usual way is to install the gem (see the {README}[link:/README] on how to do this) and
# then put at the top of your class:
#
# require 'tmail'
#
# You can then create a new TMail object in your code with:
#
# @email = TMail::Mail.new
#
# Or if you have an email as a string, you can initialize a new TMail::Mail object and get it
# to parse that string for you like so:
#
# @email = TMail::Mail.parse(email_text)
#
# You can also read a single email off the disk, for example:
#
# @email = TMail::Mail.load('filename.txt')
#
# Also, you can read a mailbox (usual unix mbox format) and end up with an array of TMail
# objects by doing something like this:
#
# # Note, we pass true as the last variable to open the mailbox read only
# mailbox = TMail::UNIXMbox.new("mailbox", nil, true)
# @emails = []
# mailbox.each_port { |m| @emails << TMail::Mail.new(m) }
#
class Mail
class << self
# Opens an email that has been saved out as a file by itself.
#
# This function will read a file non-destructively and then parse
# the contents and return a TMail::Mail object.
#
# Does not handle multiple email mailboxes (like a unix mbox) for that
# use the TMail::UNIXMbox class.
#
# Example:
# mail = TMail::Mail.load('filename')
#
def load( fname )
new(FilePort.new(fname))
end
alias load_from load
alias loadfrom load
# Parses an email from the supplied string and returns a TMail::Mail
# object.
#
# Example:
# require 'rubygems'; require 'tmail'
# email_string =<<HEREDOC
# To: [email protected]
# From: [email protected]
# Subject: This is a short Email
#
# Hello there Mikel!
#
# HEREDOC
# mail = TMail::Mail.parse(email_string)
# #=> #<TMail::Mail port=#<TMail::StringPort:id=0xa30ac0> bodyport=nil>
# mail.body
# #=> "Hello there Mikel!\n\n"
def parse( str )
new(StringPort.new(str))
end
end
def initialize( port = nil, conf = DEFAULT_CONFIG ) #:nodoc:
@port = port || StringPort.new
@config = Config.to_config(conf)
@header = {}
@body_port = nil
@body_parsed = false
@epilogue = ''
@parts = []
@port.ropen {|f|
parse_header f
parse_body f unless @port.reproducible?
}
end
# Provides access to the port this email is using to hold it's data
#
# Example:
# mail = TMail::Mail.parse(email_string)
# mail.port
# #=> #<TMail::StringPort:id=0xa2c952>
attr_reader :port
def inspect
"\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
end
#
# to_s interfaces
#
public
include StrategyInterface
def write_back( eol = "\n", charset = 'e' )
parse_body
@port.wopen {|stream| encoded eol, charset, stream }
end
def accept( strategy )
with_multipart_encoding(strategy) {
ordered_each do |name, field|
next if field.empty?
strategy.header_name canonical(name)
field.accept strategy
strategy.puts
end
strategy.puts
body_port().ropen {|r|
strategy.write r.read
}
}
end
private
def canonical( name )
name.split(/-/).map {|s| s.capitalize }.join('-')
end
def with_multipart_encoding( strategy )
if parts().empty? # DO NOT USE @parts
yield
else
bound = ::TMail.new_boundary
if @header.key? 'content-type'
@header['content-type'].params['boundary'] = bound
else
store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
end
yield
parts().each do |tm|
strategy.puts
strategy.puts '--' + bound
tm.accept strategy
end
strategy.puts
strategy.puts '--' + bound + '--'
strategy.write epilogue()
end
end
###
### header
###
public
ALLOW_MULTIPLE = {
'received' => true,
'resent-date' => true,
'resent-from' => true,
'resent-sender' => true,
'resent-to' => true,
'resent-cc' => true,
'resent-bcc' => true,
'resent-message-id' => true,
'comments' => true,
'keywords' => true
}
USE_ARRAY = ALLOW_MULTIPLE
def header
@header.dup
end
# Returns a TMail::AddressHeader object of the field you are querying.
# Examples:
# @mail['from'] #=> #<TMail::AddressHeader "[email protected]">
# @mail['to'] #=> #<TMail::AddressHeader "[email protected]">
#
# You can get the string value of this by passing "to_s" to the query:
# Example:
# @mail['to'].to_s #=> "[email protected]"
def []( key )
@header[key.downcase]
end
def sub_header(key, param)
(hdr = self[key]) ? hdr[param] : nil
end
alias fetch []
# Allows you to set or delete TMail header objects at will.
# Examples:
# @mail = TMail::Mail.new
# @mail['to'].to_s # => '[email protected]'
# @mail['to'] = '[email protected]'
# @mail['to'].to_s # => '[email protected]'
# @mail.encoded # => "To: [email protected]\r\n\r\n"
# @mail['to'] = nil
# @mail['to'].to_s # => nil
# @mail.encoded # => "\r\n"
#
# Note: setting mail[] = nil actually deletes the header field in question from the object,
# it does not just set the value of the hash to nil
def []=( key, val )
dkey = key.downcase
if val.nil?
@header.delete dkey
return nil
end
case val
when String
header = new_hf(key, val)
when HeaderField
;
when Array
ALLOW_MULTIPLE.include? dkey or
raise ArgumentError, "#{key}: Header must not be multiple"
@header[dkey] = val
return val
else
header = new_hf(key, val.to_s)
end
if ALLOW_MULTIPLE.include? dkey
(@header[dkey] ||= []).push header
else
@header[dkey] = header
end
val
end
alias store []=
# Allows you to loop through each header in the TMail::Mail object in a block
# Example:
# @mail['to'] = '[email protected]'
# @mail['from'] = '[email protected]'
# @mail.each_header { |k,v| puts "#{k} = #{v}" }
# # => from = [email protected]
# # => to = [email protected]
def each_header
@header.each do |key, val|
[val].flatten.each {|v| yield key, v }
end
end
alias each_pair each_header
def each_header_name( &block )
@header.each_key(&block)
end
alias each_key each_header_name
def each_field( &block )
@header.values.flatten.each(&block)
end
alias each_value each_field
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-disposition content-description
)
def ordered_each
list = @header.keys
FIELD_ORDER.each do |name|
if list.delete(name)
[@header[name]].flatten.each {|v| yield name, v }
end
end
list.each do |name|
[@header[name]].flatten.each {|v| yield name, v }
end
end
def clear
@header.clear
end
def delete( key )
@header.delete key.downcase
end
def delete_if
@header.delete_if do |key,val|
if Array === val
val.delete_if {|v| yield key, v }
val.empty?
else
yield key, val
end
end
end
def keys
@header.keys
end
def key?( key )
@header.key? key.downcase
end
def values_at( *args )
args.map {|k| @header[k.downcase] }.flatten
end
alias indexes values_at
alias indices values_at
private
def parse_header( f )
name = field = nil
unixfrom = nil
while line = f.gets
case line
when /\A[ \t]/ # continue from prev line
raise SyntaxError, 'mail is began by space' unless field
field << ' ' << line.strip
when /\A([^\: \t]+):\s*/ # new header line
add_hf name, field if field
name = $1
field = $' #.strip
when /\A\-*\s*\z/ # end of header
add_hf name, field if field
name = field = nil
break
when /\AFrom (\S+)/
unixfrom = $1
when /^charset=.*/
else
raise SyntaxError, "wrong mail header: '#{line.inspect}'"
end
end
add_hf name, field if name
if unixfrom
add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
end
end
def add_hf( name, field )
key = name.downcase
field = new_hf(name, field)
if ALLOW_MULTIPLE.include? key
(@header[key] ||= []).push field
else
@header[key] = field
end
end
def new_hf( name, field )
HeaderField.new(name, field, @config)
end
###
### body
###
public
def body_port
parse_body
@body_port
end
def each( &block )
body_port().ropen {|f| f.each(&block) }
end
def quoted_body
body_port.ropen {|f| return f.read }
end
def quoted_body= str
body_port.wopen { |f| f.write str }
str
end
def body=( str )
# Sets the body of the email to a new (encoded) string.
#
# We also reparses the email if the body is ever reassigned, this is a performance hit, however when
# you assign the body, you usually want to be able to make sure that you can access the attachments etc.
#
# Usage:
#
# mail.body = "Hello, this is\nthe body text"
# # => "Hello, this is\nthe body"
# mail.body
# # => "Hello, this is\nthe body"
@body_parsed = false
parse_body(StringInput.new(str))
parse_body
@body_port.wopen {|f| f.write str }
str
end
alias preamble quoted_body
alias preamble= quoted_body=
def epilogue
parse_body
@epilogue.dup
end
def epilogue=( str )
parse_body
@epilogue = str
str
end
def parts
parse_body
@parts
end
def each_part( &block )
parts().each(&block)
end
# Returns true if the content type of this part of the email is
# a disposition attachment
def disposition_is_attachment?
(self['content-disposition'] && self['content-disposition'].disposition == "attachment")
end
# Returns true if this part's content main type is text, else returns false.
# By main type is meant "text/plain" is text. "text/html" is text
def content_type_is_text?
self.header['content-type'] && (self.header['content-type'].main_type != "text")
end
private
def parse_body( f = nil )
return if @body_parsed
if f
parse_body_0 f
else
@port.ropen {|f|
skip_header f
parse_body_0 f
}
end
@body_parsed = true
end
def skip_header( f )
while line = f.gets
return if /\A[\r\n]*\z/ === line
end
end
def parse_body_0( f )
if multipart?
read_multipart f
else
@body_port = @config.new_body_port(self)
@body_port.wopen {|w|
w.write f.read
}
end
end
def read_multipart( src )
bound = @header['content-type'].params['boundary']
is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
lastbound = "--#{bound}--"
ports = [ @config.new_preamble_port(self) ]
begin
f = ports.last.wopen
while line = src.gets
if is_sep === line
f.close
break if line.strip == lastbound
ports.push @config.new_part_port(self)
f = ports.last.wopen
else
f << line
end
end
@epilogue = (src.read || '')
ensure
f.close if f and not f.closed?
end
@body_port = ports.shift
@parts = ports.map {|p| self.class.new(p, @config) }
end
end # class Mail
end # module TMail
=begin rdoc
= Mailbox and Mbox interaction class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/port'
require 'socket'
require 'mutex_m'
unless [].respond_to?(:sort_by)
module Enumerable#:nodoc:
def sort_by
map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] }
end
end
end
module TMail
class MhMailbox
PORT_CLASS = MhPort
def initialize( dir )
edir = File.expand_path(dir)
raise ArgumentError, "not directory: #{dir}"\
unless FileTest.directory? edir
@dirname = edir
@last_file = nil
@last_atime = nil
end
def directory
@dirname
end
alias dirname directory
attr_accessor :last_atime
def inspect
"#<#{self.class} #{@dirname}>"
end
def close
end
def new_port
PORT_CLASS.new(next_file_name())
end
def each_port
mail_files().each do |path|
yield PORT_CLASS.new(path)
end
@last_atime = Time.now
end
alias each each_port
def reverse_each_port
mail_files().reverse_each do |path|
yield PORT_CLASS.new(path)
end
@last_atime = Time.now
end
alias reverse_each reverse_each_port
# old #each_mail returns Port
#def each_mail
# each_port do |port|
# yield Mail.new(port)
# end
#end
def each_new_port( mtime = nil, &block )
mtime ||= @last_atime
return each_port(&block) unless mtime
return unless File.mtime(@dirname) >= mtime
mail_files().each do |path|
yield PORT_CLASS.new(path) if File.mtime(path) > mtime
end
@last_atime = Time.now
end
private
def mail_files
Dir.entries(@dirname)\
.select {|s| /\A\d+\z/ === s }\
.map {|s| s.to_i }\
.sort\
.map {|i| "#{@dirname}/#{i}" }\
.select {|path| FileTest.file? path }
end
def next_file_name
unless n = @last_file
n = 0
Dir.entries(@dirname)\
.select {|s| /\A\d+\z/ === s }\
.map {|s| s.to_i }.sort\
.each do |i|
next unless FileTest.file? "#{@dirname}/#{i}"
n = i
end
end
begin
n += 1
end while FileTest.exist? "#{@dirname}/#{n}"
@last_file = n
"#{@dirname}/#{n}"
end
end # MhMailbox
MhLoader = MhMailbox
class UNIXMbox
class << self
alias newobj new
end
# Creates a new mailbox object that you can iterate through to collect the
# emails from with "each_port".
#
# You need to pass it a filename of a unix mailbox format file, the format of this
# file can be researched at this page at {wikipedia}[link:http://en.wikipedia.org/wiki/Mbox]
#
# ==== Parameters
#
# +filename+: The filename of the mailbox you want to open
#
# +tmpdir+: Can be set to override TMail using the system environment's temp dir. TMail will first
# use the temp dir specified by you (if any) or then the temp dir specified in the Environment's TEMP
# value then the value in the Environment's TMP value or failing all of the above, '/tmp'
#
# +readonly+: If set to false, each email you take from the mail box will be removed from the mailbox.
# default is *false* - ie, it *WILL* truncate your mailbox file to ZERO once it has read the emails out.
#
# ==== Options:
#
# None
#
# ==== Examples:
#
# # First show using readonly true:
#
# require 'ftools'
# File.size("../test/fixtures/mailbox")
# #=> 20426
#
# mailbox = TMail::UNIXMbox.new("../test/fixtures/mailbox", nil, true)
# #=> #<TMail::UNIXMbox:0x14a2aa8 @readonly=true.....>
#
# mailbox.each_port do |port|
# mail = TMail::Mail.new(port)
# puts mail.subject
# end
# #Testing mailbox 1
# #Testing mailbox 2
# #Testing mailbox 3
# #Testing mailbox 4
# require 'ftools'
# File.size?("../test/fixtures/mailbox")
# #=> 20426
#
# # Now show with readonly set to the default false
#
# mailbox = TMail::UNIXMbox.new("../test/fixtures/mailbox")
# #=> #<TMail::UNIXMbox:0x14a2aa8 @readonly=false.....>
#
# mailbox.each_port do |port|
# mail = TMail::Mail.new(port)
# puts mail.subject
# end
# #Testing mailbox 1
# #Testing mailbox 2
# #Testing mailbox 3
# #Testing mailbox 4
#
# File.size?("../test/fixtures/mailbox")
# #=> nil
def UNIXMbox.new( filename, tmpdir = nil, readonly = false )
tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
newobj(filename, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false)
end
def UNIXMbox.lock( fname )
begin
f = File.open(fname, 'r+')
f.flock File::LOCK_EX
yield f
ensure
f.flock File::LOCK_UN
f.close if f and not f.closed?
end
end
def UNIXMbox.static_new( fname, dir, readonly = false )
newobj(fname, dir, readonly, true)
end
def initialize( fname, mhdir, readonly, static )
@filename = fname
@readonly = readonly
@closed = false
Dir.mkdir mhdir
@real = MhMailbox.new(mhdir)
@finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static)
ObjectSpace.define_finalizer self, @finalizer
end
def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p )
lambda {
if writeback_p
lock(mboxfile) {|f|
mh.each_port do |port|
f.puts create_from_line(port)
port.ropen {|r|
f.puts r.read
}
end
}
end
if cleanup_p
Dir.foreach(mh.dirname) do |fname|
next if /\A\.\.?\z/ === fname
File.unlink "#{mh.dirname}/#{fname}"
end
Dir.rmdir mh.dirname
end
}
end
# make _From line
def UNIXMbox.create_from_line( port )
sprintf 'From %s %s',
fromaddr(), TextUtils.time2str(File.mtime(port.filename))
end
def UNIXMbox.fromaddr(port)
h = HeaderField.new_from_port(port, 'Return-Path') ||
HeaderField.new_from_port(port, 'From') ||
HeaderField.new_from_port(port, 'EnvelopeSender') or return 'nobody'
a = h.addrs[0] or return 'nobody'
a.spec
end
def close
return if @closed
ObjectSpace.undefine_finalizer self
@finalizer.call
@finalizer = nil
@real = nil
@closed = true
@updated = nil
end
def each_port( &block )
close_check
update
@real.each_port(&block)
end
alias each each_port
def reverse_each_port( &block )
close_check
update
@real.reverse_each_port(&block)
end
alias reverse_each reverse_each_port
# old #each_mail returns Port
#def each_mail( &block )
# each_port do |port|
# yield Mail.new(port)
# end
#end
def each_new_port( mtime = nil )
close_check
update
@real.each_new_port(mtime) {|p| yield p }
end
def new_port
close_check
@real.new_port
end
private
def close_check
@closed and raise ArgumentError, 'accessing already closed mbox'
end
def update
return if FileTest.zero?(@filename)
return if @updated and File.mtime(@filename) < @updated
w = nil
port = nil
time = nil
UNIXMbox.lock(@filename) {|f|
begin
f.each do |line|
if /\AFrom / === line
w.close if w
File.utime time, time, port.filename if time
port = @real.new_port
w = port.wopen
time = fromline2time(line)
else
w.print line if w
end
end
ensure
if w and not w.closed?
w.close
File.utime time, time, port.filename if time
end
end
f.truncate(0) unless @readonly
@updated = Time.now
}
end
def fromline2time( line )
m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) \
or return nil
Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i)
end
end # UNIXMbox
MboxLoader = UNIXMbox
class Maildir
extend Mutex_m
PORT_CLASS = MaildirPort
@seq = 0
def Maildir.unique_number
synchronize {
@seq += 1
return @seq
}
end
def initialize( dir = nil )
@dirname = dir || ENV['MAILDIR']
raise ArgumentError, "not directory: #{@dirname}"\
unless FileTest.directory? @dirname
@new = "#{@dirname}/new"
@tmp = "#{@dirname}/tmp"
@cur = "#{@dirname}/cur"
end
def directory
@dirname
end
def inspect
"#<#{self.class} #{@dirname}>"
end
def close
end
def each_port
mail_files(@cur).each do |path|
yield PORT_CLASS.new(path)
end
end
alias each each_port
def reverse_each_port
mail_files(@cur).reverse_each do |path|
yield PORT_CLASS.new(path)
end
end
alias reverse_each reverse_each_port
def new_port
fname = nil
tmpfname = nil
newfname = nil
begin
fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}"
tmpfname = "#{@tmp}/#{fname}"
newfname = "#{@new}/#{fname}"
end while FileTest.exist? tmpfname
if block_given?
File.open(tmpfname, 'w') {|f| yield f }
File.rename tmpfname, newfname
PORT_CLASS.new(newfname)
else
File.open(tmpfname, 'w') {|f| f.write "\n\n" }
PORT_CLASS.new(tmpfname)
end
end
def each_new_port
mail_files(@new).each do |path|
dest = @cur + '/' + File.basename(path)
File.rename path, dest
yield PORT_CLASS.new(dest)
end
check_tmp
end
TOO_OLD = 60 * 60 * 36 # 36 hour
def check_tmp
old = Time.now.to_i - TOO_OLD
each_filename(@tmp) do |full, fname|
if FileTest.file? full and
File.stat(full).mtime.to_i < old
File.unlink full
end
end
end
private
def mail_files( dir )
Dir.entries(dir)\
.select {|s| s[0] != ?. }\
.sort_by {|s| s.slice(/\A\d+/).to_i }\
.map {|s| "#{dir}/#{s}" }\
.select {|path| FileTest.file? path }
end
def each_filename( dir )
Dir.foreach(dir) do |fname|
path = "#{dir}/#{fname}"
if fname[0] != ?. and FileTest.file? path
yield path, fname
end
end
end
end # Maildir
MaildirLoader = Maildir
end # module TMail
#:stopdoc:
require 'tmail/version'
require 'tmail/mail'
require 'tmail/mailbox'
require 'tmail/core_extensions'
#:startdoc:#:stopdoc:
require 'tmail/mailbox'
#:startdoc:#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
#:stopdoc:
require 'nkf'
#:startdoc:
module TMail
class Mail
def send_to( smtp )
do_send_to(smtp) do
ready_to_send
end
end
def send_text_to( smtp )
do_send_to(smtp) do
ready_to_send
mime_encode
end
end
def do_send_to( smtp )
from = from_address or raise ArgumentError, 'no from address'
(dests = destinations).empty? and raise ArgumentError, 'no receipient'
yield
send_to_0 smtp, from, dests
end
private :do_send_to
def send_to_0( smtp, from, to )
smtp.ready(from, to) do |f|
encoded "\r\n", 'j', f, ''
end
end
def ready_to_send
delete_no_send_fields
add_message_id
add_date
end
NOSEND_FIELDS = %w(
received
bcc
)
def delete_no_send_fields
NOSEND_FIELDS.each do |nm|
delete nm
end
delete_if {|n,v| v.empty? }
end
def add_message_id( fqdn = nil )
self.message_id = ::TMail::new_message_id(fqdn)
end
def add_date
self.date = Time.now
end
def mime_encode
if parts.empty?
mime_encode_singlepart
else
mime_encode_multipart true
end
end
def mime_encode_singlepart
self.mime_version = '1.0'
b = body
if NKF.guess(b) != NKF::BINARY
mime_encode_text b
else
mime_encode_binary b
end
end
def mime_encode_text( body )
self.body = NKF.nkf('-j -m0', body)
self.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
self.encoding = '7bit'
end
def mime_encode_binary( body )
self.body = [body].pack('m')
self.set_content_type 'application', 'octet-stream'
self.encoding = 'Base64'
end
def mime_encode_multipart( top = true )
self.mime_version = '1.0' if top
self.set_content_type 'multipart', 'mixed'
e = encoding(nil)
if e and not /\A(?:7bit|8bit|binary)\z/i === e
raise ArgumentError,
'using C.T.Encoding with multipart mail is not permitted'
end
end
end
#:stopdoc:
class DeleteFields
NOSEND_FIELDS = %w(
received
bcc
)
def initialize( nosend = nil, delempty = true )
@no_send_fields = nosend || NOSEND_FIELDS.dup
@delete_empty_fields = delempty
end
attr :no_send_fields
attr :delete_empty_fields, true
def exec( mail )
@no_send_fields.each do |nm|
delete nm
end
delete_if {|n,v| v.empty? } if @delete_empty_fields
end
end
#:startdoc:
#:stopdoc:
class AddMessageId
def initialize( fqdn = nil )
@fqdn = fqdn
end
attr :fqdn, true
def exec( mail )
mail.message_id = ::TMail::new_msgid(@fqdn)
end
end
#:startdoc:
#:stopdoc:
class AddDate
def exec( mail )
mail.date = Time.now
end
end
#:startdoc:
#:stopdoc:
class MimeEncodeAuto
def initialize( s = nil, m = nil )
@singlepart_composer = s || MimeEncodeSingle.new
@multipart_composer = m || MimeEncodeMulti.new
end
attr :singlepart_composer
attr :multipart_composer
def exec( mail )
if mail._builtin_multipart?
then @multipart_composer
else @singlepart_composer end.exec mail
end
end
#:startdoc:
#:stopdoc:
class MimeEncodeSingle
def exec( mail )
mail.mime_version = '1.0'
b = mail.body
if NKF.guess(b) != NKF::BINARY
on_text b
else
on_binary b
end
end
def on_text( body )
mail.body = NKF.nkf('-j -m0', body)
mail.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
mail.encoding = '7bit'
end
def on_binary( body )
mail.body = [body].pack('m')
mail.set_content_type 'application', 'octet-stream'
mail.encoding = 'Base64'
end
end
#:startdoc:
#:stopdoc:
class MimeEncodeMulti
def exec( mail, top = true )
mail.mime_version = '1.0' if top
mail.set_content_type 'multipart', 'mixed'
e = encoding(nil)
if e and not /\A(?:7bit|8bit|binary)\z/i === e
raise ArgumentError,
'using C.T.Encoding with multipart mail is not permitted'
end
mail.parts.each do |m|
exec m, false if m._builtin_multipart?
end
end
end
#:startdoc:
end # module TMail
=begin rdoc
= Obsolete methods that are deprecated
If you really want to see them, go to lib/tmail/obsolete.rb and view to your
heart's content.
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
#:stopdoc:
module TMail #:nodoc:
class Mail
alias include? key?
alias has_key? key?
def values
ret = []
each_field {|v| ret.push v }
ret
end
def value?( val )
HeaderField === val or return false
[ @header[val.name.downcase] ].flatten.include? val
end
alias has_value? value?
end
class Mail
def from_addr( default = nil )
addr, = from_addrs(nil)
addr || default
end
def from_address( default = nil )
if a = from_addr(nil)
a.spec
else
default
end
end
alias from_address= from_addrs=
def from_phrase( default = nil )
if a = from_addr(nil)
a.phrase
else
default
end
end
alias msgid message_id
alias msgid= message_id=
alias each_dest each_destination
end
class Address
alias route routes
alias addr spec
def spec=( str )
@local, @domain = str.split(/@/,2).map {|s| s.split(/\./) }
end
alias addr= spec=
alias address= spec=
end
class MhMailbox
alias new_mail new_port
alias each_mail each_port
alias each_newmail each_new_port
end
class UNIXMbox
alias new_mail new_port
alias each_mail each_port
alias each_newmail each_new_port
end
class Maildir
alias new_mail new_port
alias each_mail each_port
alias each_newmail each_new_port
end
extend TextUtils
class << self
alias msgid? message_id?
alias boundary new_boundary
alias msgid new_message_id
alias new_msgid new_message_id
end
def Mail.boundary
::TMail.new_boundary
end
def Mail.msgid
::TMail.new_message_id
end
end # module TMail
#:startdoc:#:stopdoc:
# DO NOT MODIFY!!!!
# This file is automatically generated by racc 1.4.5
# from racc grammer file "parser.y".
#
#
# parser.rb: generated by racc (runtime embedded)
#
###### racc/parser.rb begin
unless $".index 'racc/parser.rb'
$".push 'racc/parser.rb'
self.class.module_eval <<'..end racc/parser.rb modeval..id8076474214', 'racc/parser.rb', 1
#
# $Id: parser.rb,v 1.7 2005/11/20 17:31:32 aamine Exp $
#
# Copyright (c) 1999-2005 Minero Aoki
#
# This program is free software.
# You can distribute/modify this program under the same terms of ruby.
#
# As a special exception, when this code is copied by Racc
# into a Racc output file, you may use that output file
# without restriction.
#
unless defined?(NotImplementedError)
NotImplementedError = NotImplementError
end
module Racc
class ParseError < StandardError; end
end
unless defined?(::ParseError)
ParseError = Racc::ParseError
end
module Racc
unless defined?(Racc_No_Extentions)
Racc_No_Extentions = false
end
class Parser
old_verbose, $VERBOSE = $VERBOSE, nil
Racc_Runtime_Version = '1.4.5'
Racc_Runtime_Revision = '$Revision: 1.7 $'.split[1]
Racc_Runtime_Core_Version_R = '1.4.5'
Racc_Runtime_Core_Revision_R = '$Revision: 1.7 $'.split[1]
begin
require 'racc/cparse'
# Racc_Runtime_Core_Version_C = (defined in extention)
Racc_Runtime_Core_Revision_C = Racc_Runtime_Core_Id_C.split[2]
unless new.respond_to?(:_racc_do_parse_c, true)
raise LoadError, 'old cparse.so'
end
if Racc_No_Extentions
raise LoadError, 'selecting ruby version of racc runtime core'
end
Racc_Main_Parsing_Routine = :_racc_do_parse_c
Racc_YY_Parse_Method = :_racc_yyparse_c
Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_C
Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_C
Racc_Runtime_Type = 'c'
rescue LoadError
Racc_Main_Parsing_Routine = :_racc_do_parse_rb
Racc_YY_Parse_Method = :_racc_yyparse_rb
Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_R
Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R
Racc_Runtime_Type = 'ruby'
end
$VERBOSE = old_verbose
def Parser.racc_runtime_type
Racc_Runtime_Type
end
private
def _racc_setup
@yydebug = false unless self.class::Racc_debug_parser
@yydebug = false unless defined?(@yydebug)
if @yydebug
@racc_debug_out = $stderr unless defined?(@racc_debug_out)
@racc_debug_out ||= $stderr
end
arg = self.class::Racc_arg
arg[13] = true if arg.size < 14
arg
end
def _racc_init_sysvars
@racc_state = [0]
@racc_tstack = []
@racc_vstack = []
@racc_t = nil
@racc_val = nil
@racc_read_next = true
@racc_user_yyerror = false
@racc_error_status = 0
end
###
### do_parse
###
def do_parse
__send__(Racc_Main_Parsing_Routine, _racc_setup(), false)
end
def next_token
raise NotImplementedError, "#{self.class}\#next_token is not defined"
end
def _racc_do_parse_rb(arg, in_debug)
action_table, action_check, action_default, action_pointer,
goto_table, goto_check, goto_default, goto_pointer,
nt_base, reduce_table, token_table, shift_n,
reduce_n, use_result, * = arg
_racc_init_sysvars
tok = act = i = nil
nerr = 0
catch(:racc_end_parse) {
while true
if i = action_pointer[@racc_state[-1]]
if @racc_read_next
if @racc_t != 0 # not EOF
tok, @racc_val = next_token()
unless tok # EOF
@racc_t = 0
else
@racc_t = (token_table[tok] or 1) # error token
end
racc_read_token(@racc_t, tok, @racc_val) if @yydebug
@racc_read_next = false
end
end
i += @racc_t
unless i >= 0 and
act = action_table[i] and
action_check[i] == @racc_state[-1]
act = action_default[@racc_state[-1]]
end
else
act = action_default[@racc_state[-1]]
end
while act = _racc_evalact(act, arg)
;
end
end
}
end
###
### yyparse
###
def yyparse(recv, mid)
__send__(Racc_YY_Parse_Method, recv, mid, _racc_setup(), true)
end
def _racc_yyparse_rb(recv, mid, arg, c_debug)
action_table, action_check, action_default, action_pointer,
goto_table, goto_check, goto_default, goto_pointer,
nt_base, reduce_table, token_table, shift_n,
reduce_n, use_result, * = arg
_racc_init_sysvars
tok = nil
act = nil
i = nil
nerr = 0
catch(:racc_end_parse) {
until i = action_pointer[@racc_state[-1]]
while act = _racc_evalact(action_default[@racc_state[-1]], arg)
;
end
end
recv.__send__(mid) do |tok, val|
unless tok
@racc_t = 0
else
@racc_t = (token_table[tok] or 1) # error token
end
@racc_val = val
@racc_read_next = false
i += @racc_t
unless i >= 0 and
act = action_table[i] and
action_check[i] == @racc_state[-1]
act = action_default[@racc_state[-1]]
end
while act = _racc_evalact(act, arg)
;
end
while not (i = action_pointer[@racc_state[-1]]) or
not @racc_read_next or
@racc_t == 0 # $
unless i and i += @racc_t and
i >= 0 and
act = action_table[i] and
action_check[i] == @racc_state[-1]
act = action_default[@racc_state[-1]]
end
while act = _racc_evalact(act, arg)
;
end
end
end
}
end
###
### common
###
def _racc_evalact(act, arg)
action_table, action_check, action_default, action_pointer,
goto_table, goto_check, goto_default, goto_pointer,
nt_base, reduce_table, token_table, shift_n,
reduce_n, use_result, * = arg
nerr = 0 # tmp
if act > 0 and act < shift_n
#
# shift
#
if @racc_error_status > 0
@racc_error_status -= 1 unless @racc_t == 1 # error token
end
@racc_vstack.push @racc_val
@racc_state.push act
@racc_read_next = true
if @yydebug
@racc_tstack.push @racc_t
racc_shift @racc_t, @racc_tstack, @racc_vstack
end
elsif act < 0 and act > -reduce_n
#
# reduce
#
code = catch(:racc_jump) {
@racc_state.push _racc_do_reduce(arg, act)
false
}
if code
case code
when 1 # yyerror
@racc_user_yyerror = true # user_yyerror
return -reduce_n
when 2 # yyaccept
return shift_n
else
raise '[Racc Bug] unknown jump code'
end
end
elsif act == shift_n
#
# accept
#
racc_accept if @yydebug
throw :racc_end_parse, @racc_vstack[0]
elsif act == -reduce_n
#
# error
#
case @racc_error_status
when 0
unless arg[21] # user_yyerror
nerr += 1
on_error @racc_t, @racc_val, @racc_vstack
end
when 3
if @racc_t == 0 # is $
throw :racc_end_parse, nil
end
@racc_read_next = true
end
@racc_user_yyerror = false
@racc_error_status = 3
while true
if i = action_pointer[@racc_state[-1]]
i += 1 # error token
if i >= 0 and
(act = action_table[i]) and
action_check[i] == @racc_state[-1]
break
end
end
throw :racc_end_parse, nil if @racc_state.size <= 1
@racc_state.pop
@racc_vstack.pop
if @yydebug
@racc_tstack.pop
racc_e_pop @racc_state, @racc_tstack, @racc_vstack
end
end
return act
else
raise "[Racc Bug] unknown action #{act.inspect}"
end
racc_next_state(@racc_state[-1], @racc_state) if @yydebug
nil
end
def _racc_do_reduce(arg, act)
action_table, action_check, action_default, action_pointer,
goto_table, goto_check, goto_default, goto_pointer,
nt_base, reduce_table, token_table, shift_n,
reduce_n, use_result, * = arg
state = @racc_state
vstack = @racc_vstack
tstack = @racc_tstack
i = act * -3
len = reduce_table[i]
reduce_to = reduce_table[i+1]
method_id = reduce_table[i+2]
void_array = []
tmp_t = tstack[-len, len] if @yydebug
tmp_v = vstack[-len, len]
tstack[-len, len] = void_array if @yydebug
vstack[-len, len] = void_array
state[-len, len] = void_array
# tstack must be updated AFTER method call
if use_result
vstack.push __send__(method_id, tmp_v, vstack, tmp_v[0])
else
vstack.push __send__(method_id, tmp_v, vstack)
end
tstack.push reduce_to
racc_reduce(tmp_t, reduce_to, tstack, vstack) if @yydebug
k1 = reduce_to - nt_base
if i = goto_pointer[k1]
i += state[-1]
if i >= 0 and (curstate = goto_table[i]) and goto_check[i] == k1
return curstate
end
end
goto_default[k1]
end
def on_error(t, val, vstack)
raise ParseError, sprintf("\nparse error on value %s (%s)",
val.inspect, token_to_str(t) || '?')
end
def yyerror
throw :racc_jump, 1
end
def yyaccept
throw :racc_jump, 2
end
def yyerrok
@racc_error_status = 0
end
#
# for debugging output
#
def racc_read_token(t, tok, val)
@racc_debug_out.print 'read '
@racc_debug_out.print tok.inspect, '(', racc_token2str(t), ') '
@racc_debug_out.puts val.inspect
@racc_debug_out.puts
end
def racc_shift(tok, tstack, vstack)
@racc_debug_out.puts "shift #{racc_token2str tok}"
racc_print_stacks tstack, vstack
@racc_debug_out.puts
end
def racc_reduce(toks, sim, tstack, vstack)
out = @racc_debug_out
out.print 'reduce '
if toks.empty?
out.print ' <none>'
else
toks.each {|t| out.print ' ', racc_token2str(t) }
end
out.puts " --> #{racc_token2str(sim)}"
racc_print_stacks tstack, vstack
@racc_debug_out.puts
end
def racc_accept
@racc_debug_out.puts 'accept'
@racc_debug_out.puts
end
def racc_e_pop(state, tstack, vstack)
@racc_debug_out.puts 'error recovering mode: pop token'
racc_print_states state
racc_print_stacks tstack, vstack
@racc_debug_out.puts
end
def racc_next_state(curstate, state)
@racc_debug_out.puts "goto #{curstate}"
racc_print_states state
@racc_debug_out.puts
end
def racc_print_stacks(t, v)
out = @racc_debug_out
out.print ' ['
t.each_index do |i|
out.print ' (', racc_token2str(t[i]), ' ', v[i].inspect, ')'
end
out.puts ' ]'
end
def racc_print_states(s)
out = @racc_debug_out
out.print ' ['
s.each {|st| out.print ' ', st }
out.puts ' ]'
end
def racc_token2str(tok)
self.class::Racc_token_to_s_table[tok] or
raise "[Racc Bug] can't convert token #{tok} to string"
end
def token_to_str(t)
self.class::Racc_token_to_s_table[t]
end
end
end
..end racc/parser.rb modeval..id8076474214
end
###### racc/parser.rb end
#
# parser.rb
#
# Copyright (c) 1998-2007 Minero Aoki
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2.1.
#
require 'tmail/scanner'
require 'tmail/utils'
module TMail
class Parser < Racc::Parser
module_eval <<'..end parser.y modeval..id7b0b3dccb7', 'parser.y', 340
include TextUtils
def self.parse( ident, str, cmt = nil )
new.parse(ident, str, cmt)
end
MAILP_DEBUG = false
def initialize
self.debug = MAILP_DEBUG
end
def debug=( flag )
@yydebug = flag && Racc_debug_parser
@scanner_debug = flag
end
def debug
@yydebug
end
def parse( ident, str, comments = nil )
@scanner = Scanner.new(str, ident, comments)
@scanner.debug = @scanner_debug
@first = [ident, ident]
result = yyparse(self, :parse_in)
comments.map! {|c| to_kcode(c) } if comments
result
end
private
def parse_in( &block )
yield @first
@scanner.scan(&block)
end
def on_error( t, val, vstack )
raise SyntaxError, "parse error on token #{racc_token2str t}"
end
..end parser.y modeval..id7b0b3dccb7
##### racc 1.4.5 generates ###
racc_reduce_table = [
0, 0, :racc_error,
2, 35, :_reduce_1,
2, 35, :_reduce_2,
2, 35, :_reduce_3,
2, 35, :_reduce_4,
2, 35, :_reduce_5,
2, 35, :_reduce_6,
2, 35, :_reduce_7,
2, 35, :_reduce_8,
2, 35, :_reduce_9,
2, 35, :_reduce_10,
2, 35, :_reduce_11,
2, 35, :_reduce_12,
6, 36, :_reduce_13,
0, 48, :_reduce_none,
2, 48, :_reduce_none,
3, 49, :_reduce_16,
5, 49, :_reduce_17,
1, 50, :_reduce_18,
7, 37, :_reduce_19,
0, 51, :_reduce_none,
2, 51, :_reduce_21,
0, 52, :_reduce_none,
2, 52, :_reduce_23,
1, 58, :_reduce_24,
3, 58, :_reduce_25,
2, 58, :_reduce_26,
0, 53, :_reduce_none,
2, 53, :_reduce_28,
0, 54, :_reduce_29,
3, 54, :_reduce_30,
0, 55, :_reduce_none,
2, 55, :_reduce_32,
2, 55, :_reduce_33,
0, 56, :_reduce_none,
2, 56, :_reduce_35,
1, 61, :_reduce_36,
1, 61, :_reduce_37,
0, 57, :_reduce_none,
2, 57, :_reduce_39,
1, 38, :_reduce_none,
1, 38, :_reduce_none,
3, 38, :_reduce_none,
1, 46, :_reduce_none,
1, 46, :_reduce_none,
1, 46, :_reduce_none,
1, 39, :_reduce_none,
2, 39, :_reduce_47,
1, 64, :_reduce_48,
3, 64, :_reduce_49,
1, 68, :_reduce_none,
1, 68, :_reduce_none,
1, 69, :_reduce_52,
3, 69, :_reduce_53,
1, 47, :_reduce_none,
1, 47, :_reduce_none,
2, 47, :_reduce_56,
2, 67, :_reduce_none,
3, 65, :_reduce_58,
2, 65, :_reduce_59,
1, 70, :_reduce_60,
2, 70, :_reduce_61,
4, 62, :_reduce_62,
3, 62, :_reduce_63,
2, 72, :_reduce_none,
2, 73, :_reduce_65,
4, 73, :_reduce_66,
3, 63, :_reduce_67,
1, 63, :_reduce_68,
1, 74, :_reduce_none,
2, 74, :_reduce_70,
1, 71, :_reduce_71,
3, 71, :_reduce_72,
1, 59, :_reduce_73,
3, 59, :_reduce_74,
1, 76, :_reduce_75,
2, 76, :_reduce_76,
1, 75, :_reduce_none,
1, 75, :_reduce_none,
1, 75, :_reduce_none,
1, 77, :_reduce_none,
1, 77, :_reduce_none,
1, 77, :_reduce_none,
1, 66, :_reduce_none,
2, 66, :_reduce_none,
3, 60, :_reduce_85,
1, 40, :_reduce_86,
3, 40, :_reduce_87,
1, 79, :_reduce_none,
2, 79, :_reduce_89,
1, 41, :_reduce_90,
2, 41, :_reduce_91,
3, 42, :_reduce_92,
5, 43, :_reduce_93,
3, 43, :_reduce_94,
0, 80, :_reduce_95,
5, 80, :_reduce_96,
5, 80, :_reduce_97,
1, 44, :_reduce_98,
3, 45, :_reduce_99,
0, 81, :_reduce_none,
1, 81, :_reduce_none,
1, 78, :_reduce_none,
1, 78, :_reduce_none,
1, 78, :_reduce_none,
1, 78, :_reduce_none,
1, 78, :_reduce_none,
1, 78, :_reduce_none,
1, 78, :_reduce_none ]
racc_reduce_n = 109
racc_shift_n = 167
racc_action_table = [
-70, -69, 23, 25, 145, 146, 29, 31, 105, 106,
16, 17, 20, 22, 136, 27, -70, -69, 32, 101,
-70, -69, 153, 100, 113, 115, -70, -69, -70, 109,
75, 23, 25, 101, 154, 29, 31, 142, 143, 16,
17, 20, 22, 107, 27, 23, 25, 32, 98, 29,
31, 96, 94, 16, 17, 20, 22, 78, 27, 23,
25, 32, 112, 29, 31, 74, 91, 16, 17, 20,
22, 88, 117, 92, 81, 32, 23, 25, 80, 123,
29, 31, 100, 125, 16, 17, 20, 22, 126, 23,
25, 109, 32, 29, 31, 91, 128, 16, 17, 20,
22, 129, 27, 23, 25, 32, 101, 29, 31, 101,
130, 16, 17, 20, 22, 79, 52, 23, 25, 32,
78, 29, 31, 133, 78, 16, 17, 20, 22, 77,
23, 25, 75, 32, 29, 31, 65, 62, 16, 17,
20, 22, 139, 23, 25, 101, 32, 29, 31, 60,
100, 16, 17, 20, 22, 44, 27, 101, 147, 32,
23, 25, 120, 148, 29, 31, 151, 152, 16, 17,
20, 22, 42, 27, 156, 158, 32, 23, 25, 120,
40, 29, 31, 15, 163, 16, 17, 20, 22, 40,
27, 23, 25, 32, 68, 29, 31, 165, 166, 16,
17, 20, 22, nil, 27, 23, 25, 32, nil, 29,
31, 74, nil, 16, 17, 20, 22, nil, 23, 25,
nil, 32, 29, 31, nil, nil, 16, 17, 20, 22,
nil, 23, 25, nil, 32, 29, 31, nil, nil, 16,
17, 20, 22, nil, 23, 25, nil, 32, 29, 31,
nil, nil, 16, 17, 20, 22, nil, 23, 25, nil,
32, 29, 31, nil, nil, 16, 17, 20, 22, nil,
27, 23, 25, 32, nil, 29, 31, nil, nil, 16,
17, 20, 22, nil, 23, 25, nil, 32, 29, 31,
nil, nil, 16, 17, 20, 22, nil, 23, 25, nil,
32, 29, 31, nil, nil, 16, 17, 20, 22, nil,
84, 25, nil, 32, 29, 31, nil, 87, 16, 17,
20, 22, 4, 6, 7, 8, 9, 10, 11, 12,
13, 1, 2, 3, 84, 25, nil, nil, 29, 31,
nil, 87, 16, 17, 20, 22, 84, 25, nil, nil,
29, 31, nil, 87, 16, 17, 20, 22, 84, 25,
nil, nil, 29, 31, nil, 87, 16, 17, 20, 22,
84, 25, nil, nil, 29, 31, nil, 87, 16, 17,
20, 22, 84, 25, nil, nil, 29, 31, nil, 87,
16, 17, 20, 22, 84, 25, nil, nil, 29, 31,
nil, 87, 16, 17, 20, 22 ]
racc_action_check = [
75, 28, 68, 68, 136, 136, 68, 68, 72, 72,
68, 68, 68, 68, 126, 68, 75, 28, 68, 67,
75, 28, 143, 66, 86, 86, 75, 28, 75, 75,
28, 3, 3, 86, 143, 3, 3, 134, 134, 3,
3, 3, 3, 73, 3, 151, 151, 3, 62, 151,
151, 60, 56, 151, 151, 151, 151, 51, 151, 52,
52, 151, 80, 52, 52, 52, 50, 52, 52, 52,
52, 45, 89, 52, 42, 52, 71, 71, 41, 96,
71, 71, 97, 98, 71, 71, 71, 71, 100, 7,
7, 101, 71, 7, 7, 102, 104, 7, 7, 7,
7, 105, 7, 8, 8, 7, 108, 8, 8, 111,
112, 8, 8, 8, 8, 40, 8, 9, 9, 8,
36, 9, 9, 117, 121, 9, 9, 9, 9, 33,
10, 10, 70, 9, 10, 10, 13, 12, 10, 10,
10, 10, 130, 2, 2, 131, 10, 2, 2, 11,
135, 2, 2, 2, 2, 6, 2, 138, 139, 2,
90, 90, 90, 140, 90, 90, 141, 142, 90, 90,
90, 90, 5, 90, 147, 150, 90, 127, 127, 127,
4, 127, 127, 1, 156, 127, 127, 127, 127, 158,
127, 26, 26, 127, 26, 26, 26, 162, 163, 26,
26, 26, 26, nil, 26, 27, 27, 26, nil, 27,
27, 27, nil, 27, 27, 27, 27, nil, 154, 154,
nil, 27, 154, 154, nil, nil, 154, 154, 154, 154,
nil, 122, 122, nil, 154, 122, 122, nil, nil, 122,
122, 122, 122, nil, 76, 76, nil, 122, 76, 76,
nil, nil, 76, 76, 76, 76, nil, 38, 38, nil,
76, 38, 38, nil, nil, 38, 38, 38, 38, nil,
38, 55, 55, 38, nil, 55, 55, nil, nil, 55,
55, 55, 55, nil, 94, 94, nil, 55, 94, 94,
nil, nil, 94, 94, 94, 94, nil, 59, 59, nil,
94, 59, 59, nil, nil, 59, 59, 59, 59, nil,
114, 114, nil, 59, 114, 114, nil, 114, 114, 114,
114, 114, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 77, 77, nil, nil, 77, 77,
nil, 77, 77, 77, 77, 77, 44, 44, nil, nil,
44, 44, nil, 44, 44, 44, 44, 44, 113, 113,
nil, nil, 113, 113, nil, 113, 113, 113, 113, 113,
88, 88, nil, nil, 88, 88, nil, 88, 88, 88,
88, 88, 74, 74, nil, nil, 74, 74, nil, 74,
74, 74, 74, 74, 129, 129, nil, nil, 129, 129,
nil, 129, 129, 129, 129, 129 ]
racc_action_pointer = [
320, 152, 129, 17, 165, 172, 137, 75, 89, 103,
116, 135, 106, 105, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, 177, 191, 1, nil,
nil, nil, nil, 109, nil, nil, 94, nil, 243, nil,
99, 64, 74, nil, 332, 52, nil, nil, nil, nil,
50, 31, 45, nil, nil, 257, 36, nil, nil, 283,
22, nil, 16, nil, nil, nil, -3, -10, -12, nil,
103, 62, -8, 15, 368, 0, 230, 320, nil, nil,
47, nil, nil, nil, nil, nil, 4, nil, 356, 50,
146, nil, nil, nil, 270, nil, 65, 56, 52, nil,
57, 62, 79, nil, 68, 81, nil, nil, 77, nil,
nil, 80, 96, 344, 296, nil, nil, 108, nil, nil,
nil, 98, 217, nil, nil, nil, -19, 163, nil, 380,
128, 116, nil, nil, 14, 124, -26, nil, 128, 141,
148, 141, 152, 7, nil, nil, nil, 160, nil, nil,
149, 31, nil, nil, 204, nil, 167, nil, 174, nil,
nil, nil, 169, 184, nil, nil, nil ]
racc_action_default = [
-109, -109, -109, -109, -14, -109, -20, -109, -109, -109,
-109, -109, -109, -109, -10, -95, -105, -106, -77, -44,
-107, -11, -108, -79, -43, -102, -109, -109, -60, -103,
-55, -104, -78, -68, -54, -71, -45, -12, -109, -1,
-109, -109, -109, -2, -109, -22, -51, -48, -50, -3,
-40, -41, -109, -46, -4, -86, -5, -88, -6, -90,
-109, -7, -95, -8, -9, -98, -100, -61, -59, -56,
-69, -109, -109, -109, -109, -75, -109, -109, -57, -15,
-109, 167, -73, -80, -82, -21, -24, -81, -109, -27,
-109, -83, -47, -89, -109, -91, -109, -100, -109, -99,
-101, -75, -58, -52, -109, -109, -64, -63, -65, -76,
-72, -67, -109, -109, -109, -26, -23, -109, -29, -49,
-84, -42, -87, -92, -94, -95, -109, -109, -62, -109,
-109, -25, -74, -28, -31, -100, -109, -53, -66, -109,
-109, -34, -109, -109, -93, -96, -97, -109, -18, -13,
-38, -109, -30, -33, -109, -32, -16, -19, -14, -35,
-36, -37, -109, -109, -39, -85, -17 ]
racc_goto_table = [
39, 67, 70, 73, 38, 66, 69, 24, 37, 57,
59, 36, 55, 67, 99, 90, 85, 157, 69, 108,
83, 134, 111, 76, 49, 53, 141, 70, 73, 150,
118, 89, 45, 155, 159, 149, 140, 21, 14, 19,
119, 102, 64, 63, 61, 124, 70, 104, 58, 132,
83, 56, 97, 83, 54, 93, 43, 5, 131, 95,
116, nil, 76, nil, 83, 76, nil, 127, nil, 38,
nil, nil, nil, 103, 138, nil, 110, nil, nil, nil,
nil, nil, nil, 144, nil, nil, nil, nil, nil, 83,
83, nil, nil, nil, 57, nil, nil, 122, nil, 121,
nil, nil, nil, nil, nil, 83, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 135, nil, nil, nil, nil,
nil, nil, 93, nil, nil, nil, 70, 161, 38, 70,
162, 160, 137, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, 164 ]
racc_goto_check = [
2, 37, 37, 29, 36, 46, 28, 13, 13, 41,
41, 31, 45, 37, 47, 32, 24, 23, 28, 25,
44, 20, 25, 42, 4, 4, 21, 37, 29, 22,
19, 18, 17, 26, 27, 16, 15, 12, 11, 33,
34, 35, 10, 9, 8, 47, 37, 29, 7, 43,
44, 6, 46, 44, 5, 41, 3, 1, 25, 41,
24, nil, 42, nil, 44, 42, nil, 32, nil, 36,
nil, nil, nil, 13, 25, nil, 41, nil, nil, nil,
nil, nil, nil, 47, nil, nil, nil, nil, nil, 44,
44, nil, nil, nil, 41, nil, nil, 45, nil, 31,
nil, nil, nil, nil, nil, 44, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 46, nil, nil, nil, nil,
nil, nil, 41, nil, nil, nil, 37, 29, 36, 37,
29, 28, 13, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, 2 ]
racc_goto_pointer = [
nil, 57, -4, 50, 17, 46, 42, 38, 33, 31,
29, 37, 35, 5, nil, -94, -105, 26, -14, -59,
-97, -108, -112, -133, -28, -55, -110, -117, -20, -24,
nil, 9, -35, 37, -50, -27, 1, -25, nil, nil,
nil, 0, -5, -65, -24, 3, -10, -52 ]
racc_goto_default = [
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, 48, 41, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 86, nil, nil, 30, 34,
50, 51, nil, 46, 47, nil, 26, 28, 71, 72,
33, 35, 114, 82, 18, nil, nil, nil ]
racc_token_table = {
false => 0,
Object.new => 1,
:DATETIME => 2,
:RECEIVED => 3,
:MADDRESS => 4,
:RETPATH => 5,
:KEYWORDS => 6,
:ENCRYPTED => 7,
:MIMEVERSION => 8,
:CTYPE => 9,
:CENCODING => 10,
:CDISPOSITION => 11,
:ADDRESS => 12,
:MAILBOX => 13,
:DIGIT => 14,
:ATOM => 15,
"," => 16,
":" => 17,
:FROM => 18,
:BY => 19,
"@" => 20,
:DOMLIT => 21,
:VIA => 22,
:WITH => 23,
:ID => 24,
:FOR => 25,
";" => 26,
"<" => 27,
">" => 28,
"." => 29,
:QUOTED => 30,
:TOKEN => 31,
"/" => 32,
"=" => 33 }
racc_use_result_var = false
racc_nt_base = 34
Racc_arg = [
racc_action_table,
racc_action_check,
racc_action_default,
racc_action_pointer,
racc_goto_table,
racc_goto_check,
racc_goto_default,
racc_goto_pointer,
racc_nt_base,
racc_reduce_table,
racc_token_table,
racc_shift_n,
racc_reduce_n,
racc_use_result_var ]
Racc_token_to_s_table = [
'$end',
'error',
'DATETIME',
'RECEIVED',
'MADDRESS',
'RETPATH',
'KEYWORDS',
'ENCRYPTED',
'MIMEVERSION',
'CTYPE',
'CENCODING',
'CDISPOSITION',
'ADDRESS',
'MAILBOX',
'DIGIT',
'ATOM',
'","',
'":"',
'FROM',
'BY',
'"@"',
'DOMLIT',
'VIA',
'WITH',
'ID',
'FOR',
'";"',
'"<"',
'">"',
'"."',
'QUOTED',
'TOKEN',
'"/"',
'"="',
'$start',
'content',
'datetime',
'received',
'addrs_TOP',
'retpath',
'keys',
'enc',
'version',
'ctype',
'cencode',
'cdisp',
'addr_TOP',
'mbox',
'day',
'hour',
'zone',
'from',
'by',
'via',
'with',
'id',
'for',
'received_datetime',
'received_domain',
'domain',
'msgid',
'received_addrspec',
'routeaddr',
'spec',
'addrs',
'group_bare',
'commas',
'group',
'addr',
'mboxes',
'addr_phrase',
'local_head',
'routes',
'at_domains',
'local',
'word',
'dots',
'domword',
'atom',
'phrase',
'params',
'opt_semicolon']
Racc_debug_parser = false
##### racc system variables end #####
# reduce 0 omitted
module_eval <<'.,.,', 'parser.y', 16
def _reduce_1( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 17
def _reduce_2( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 18
def _reduce_3( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 19
def _reduce_4( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 20
def _reduce_5( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 21
def _reduce_6( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 22
def _reduce_7( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 23
def _reduce_8( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 24
def _reduce_9( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 25
def _reduce_10( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 26
def _reduce_11( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 27
def _reduce_12( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 36
def _reduce_13( val, _values)
t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0)
(t + val[4] - val[5]).localtime
end
.,.,
# reduce 14 omitted
# reduce 15 omitted
module_eval <<'.,.,', 'parser.y', 45
def _reduce_16( val, _values)
(val[0].to_i * 60 * 60) +
(val[2].to_i * 60)
end
.,.,
module_eval <<'.,.,', 'parser.y', 51
def _reduce_17( val, _values)
(val[0].to_i * 60 * 60) +
(val[2].to_i * 60) +
(val[4].to_i)
end
.,.,
module_eval <<'.,.,', 'parser.y', 56
def _reduce_18( val, _values)
timezone_string_to_unixtime(val[0])
end
.,.,
module_eval <<'.,.,', 'parser.y', 61
def _reduce_19( val, _values)
val
end
.,.,
# reduce 20 omitted
module_eval <<'.,.,', 'parser.y', 67
def _reduce_21( val, _values)
val[1]
end
.,.,
# reduce 22 omitted
module_eval <<'.,.,', 'parser.y', 73
def _reduce_23( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 79
def _reduce_24( val, _values)
join_domain(val[0])
end
.,.,
module_eval <<'.,.,', 'parser.y', 83
def _reduce_25( val, _values)
join_domain(val[2])
end
.,.,
module_eval <<'.,.,', 'parser.y', 87
def _reduce_26( val, _values)
join_domain(val[0])
end
.,.,
# reduce 27 omitted
module_eval <<'.,.,', 'parser.y', 93
def _reduce_28( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 98
def _reduce_29( val, _values)
[]
end
.,.,
module_eval <<'.,.,', 'parser.y', 103
def _reduce_30( val, _values)
val[0].push val[2]
val[0]
end
.,.,
# reduce 31 omitted
module_eval <<'.,.,', 'parser.y', 109
def _reduce_32( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 113
def _reduce_33( val, _values)
val[1]
end
.,.,
# reduce 34 omitted
module_eval <<'.,.,', 'parser.y', 119
def _reduce_35( val, _values)
val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 125
def _reduce_36( val, _values)
val[0].spec
end
.,.,
module_eval <<'.,.,', 'parser.y', 129
def _reduce_37( val, _values)
val[0].spec
end
.,.,
# reduce 38 omitted
module_eval <<'.,.,', 'parser.y', 136
def _reduce_39( val, _values)
val[1]
end
.,.,
# reduce 40 omitted
# reduce 41 omitted
# reduce 42 omitted
# reduce 43 omitted
# reduce 44 omitted
# reduce 45 omitted
# reduce 46 omitted
module_eval <<'.,.,', 'parser.y', 146
def _reduce_47( val, _values)
[ Address.new(nil, nil) ]
end
.,.,
module_eval <<'.,.,', 'parser.y', 152
def _reduce_48( val, _values)
val
end
.,.,
module_eval <<'.,.,', 'parser.y', 157
def _reduce_49( val, _values)
val[0].push val[2]
val[0]
end
.,.,
# reduce 50 omitted
# reduce 51 omitted
module_eval <<'.,.,', 'parser.y', 165
def _reduce_52( val, _values)
val
end
.,.,
module_eval <<'.,.,', 'parser.y', 170
def _reduce_53( val, _values)
val[0].push val[2]
val[0]
end
.,.,
# reduce 54 omitted
# reduce 55 omitted
module_eval <<'.,.,', 'parser.y', 178
def _reduce_56( val, _values)
val[1].phrase = Decoder.decode(val[0])
val[1]
end
.,.,
# reduce 57 omitted
module_eval <<'.,.,', 'parser.y', 185
def _reduce_58( val, _values)
AddressGroup.new(val[0], val[2])
end
.,.,
module_eval <<'.,.,', 'parser.y', 185
def _reduce_59( val, _values)
AddressGroup.new(val[0], [])
end
.,.,
module_eval <<'.,.,', 'parser.y', 188
def _reduce_60( val, _values)
val[0].join('.')
end
.,.,
module_eval <<'.,.,', 'parser.y', 189
def _reduce_61( val, _values)
val[0] << ' ' << val[1].join('.')
end
.,.,
module_eval <<'.,.,', 'parser.y', 196
def _reduce_62( val, _values)
val[2].routes.replace val[1]
val[2]
end
.,.,
module_eval <<'.,.,', 'parser.y', 200
def _reduce_63( val, _values)
val[1]
end
.,.,
# reduce 64 omitted
module_eval <<'.,.,', 'parser.y', 203
def _reduce_65( val, _values)
[ val[1].join('.') ]
end
.,.,
module_eval <<'.,.,', 'parser.y', 204
def _reduce_66( val, _values)
val[0].push val[3].join('.'); val[0]
end
.,.,
module_eval <<'.,.,', 'parser.y', 206
def _reduce_67( val, _values)
Address.new( val[0], val[2] )
end
.,.,
module_eval <<'.,.,', 'parser.y', 207
def _reduce_68( val, _values)
Address.new( val[0], nil )
end
.,.,
# reduce 69 omitted
module_eval <<'.,.,', 'parser.y', 210
def _reduce_70( val, _values)
val[0].push ''; val[0]
end
.,.,
module_eval <<'.,.,', 'parser.y', 213
def _reduce_71( val, _values)
val
end
.,.,
module_eval <<'.,.,', 'parser.y', 222
def _reduce_72( val, _values)
val[1].times do
val[0].push ''
end
val[0].push val[2]
val[0]
end
.,.,
module_eval <<'.,.,', 'parser.y', 224
def _reduce_73( val, _values)
val
end
.,.,
module_eval <<'.,.,', 'parser.y', 233
def _reduce_74( val, _values)
val[1].times do
val[0].push ''
end
val[0].push val[2]
val[0]
end
.,.,
module_eval <<'.,.,', 'parser.y', 234
def _reduce_75( val, _values)
0
end
.,.,
module_eval <<'.,.,', 'parser.y', 235
def _reduce_76( val, _values)
1
end
.,.,
# reduce 77 omitted
# reduce 78 omitted
# reduce 79 omitted
# reduce 80 omitted
# reduce 81 omitted
# reduce 82 omitted
# reduce 83 omitted
# reduce 84 omitted
module_eval <<'.,.,', 'parser.y', 253
def _reduce_85( val, _values)
val[1] = val[1].spec
val.join('')
end
.,.,
module_eval <<'.,.,', 'parser.y', 254
def _reduce_86( val, _values)
val
end
.,.,
module_eval <<'.,.,', 'parser.y', 255
def _reduce_87( val, _values)
val[0].push val[2]; val[0]
end
.,.,
# reduce 88 omitted
module_eval <<'.,.,', 'parser.y', 258
def _reduce_89( val, _values)
val[0] << ' ' << val[1]
end
.,.,
module_eval <<'.,.,', 'parser.y', 265
def _reduce_90( val, _values)
val.push nil
val
end
.,.,
module_eval <<'.,.,', 'parser.y', 269
def _reduce_91( val, _values)
val
end
.,.,
module_eval <<'.,.,', 'parser.y', 274
def _reduce_92( val, _values)
[ val[0].to_i, val[2].to_i ]
end
.,.,
module_eval <<'.,.,', 'parser.y', 279
def _reduce_93( val, _values)
[ val[0].downcase, val[2].downcase, decode_params(val[3]) ]
end
.,.,
module_eval <<'.,.,', 'parser.y', 283
def _reduce_94( val, _values)
[ val[0].downcase, nil, decode_params(val[1]) ]
end
.,.,
module_eval <<'.,.,', 'parser.y', 288
def _reduce_95( val, _values)
{}
end
.,.,
module_eval <<'.,.,', 'parser.y', 293
def _reduce_96( val, _values)
val[0][ val[2].downcase ] = ('"' + val[4].to_s + '"')
val[0]
end
.,.,
module_eval <<'.,.,', 'parser.y', 298
def _reduce_97( val, _values)
val[0][ val[2].downcase ] = val[4]
val[0]
end
.,.,
module_eval <<'.,.,', 'parser.y', 303
def _reduce_98( val, _values)
val[0].downcase
end
.,.,
module_eval <<'.,.,', 'parser.y', 308
def _reduce_99( val, _values)
[ val[0].downcase, decode_params(val[1]) ]
end
.,.,
# reduce 100 omitted
# reduce 101 omitted
# reduce 102 omitted
# reduce 103 omitted
# reduce 104 omitted
# reduce 105 omitted
# reduce 106 omitted
# reduce 107 omitted
# reduce 108 omitted
def _reduce_none( val, _values)
val[0]
end
end # class Parser
end # module TMail
=begin rdoc
= Port class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/stringio'
module TMail
class Port
def reproducible?
false
end
end
###
### FilePort
###
class FilePort < Port
def initialize( fname )
@filename = File.expand_path(fname)
super()
end
attr_reader :filename
alias ident filename
def ==( other )
other.respond_to?(:filename) and @filename == other.filename
end
alias eql? ==
def hash
@filename.hash
end
def inspect
"#<#{self.class}:#{@filename}>"
end
def reproducible?
true
end
def size
File.size @filename
end
def ropen( &block )
File.open(@filename, &block)
end
def wopen( &block )
File.open(@filename, 'w', &block)
end
def aopen( &block )
File.open(@filename, 'a', &block)
end
def read_all
ropen {|f|
return f.read
}
end
def remove
File.unlink @filename
end
def move_to( port )
begin
File.link @filename, port.filename
rescue Errno::EXDEV
copy_to port
end
File.unlink @filename
end
alias mv move_to
def copy_to( port )
if FilePort === port
copy_file @filename, port.filename
else
File.open(@filename) {|r|
port.wopen {|w|
while s = r.sysread(4096)
w.write << s
end
} }
end
end
alias cp copy_to
private
# from fileutils.rb
def copy_file( src, dest )
st = r = w = nil
File.open(src, 'rb') {|r|
File.open(dest, 'wb') {|w|
st = r.stat
begin
while true
w.write r.sysread(st.blksize)
end
rescue EOFError
end
} }
end
end
module MailFlags
def seen=( b )
set_status 'S', b
end
def seen?
get_status 'S'
end
def replied=( b )
set_status 'R', b
end
def replied?
get_status 'R'
end
def flagged=( b )
set_status 'F', b
end
def flagged?
get_status 'F'
end
private
def procinfostr( str, tag, true_p )
a = str.upcase.split(//)
a.push true_p ? tag : nil
a.delete tag unless true_p
a.compact.sort.join('').squeeze
end
end
class MhPort < FilePort
include MailFlags
private
def set_status( tag, flag )
begin
tmpfile = @filename + '.tmailtmp.' + $$.to_s
File.open(tmpfile, 'w') {|f|
write_status f, tag, flag
}
File.unlink @filename
File.link tmpfile, @filename
ensure
File.unlink tmpfile
end
end
def write_status( f, tag, flag )
stat = ''
File.open(@filename) {|r|
while line = r.gets
if line.strip.empty?
break
elsif m = /\AX-TMail-Status:/i.match(line)
stat = m.post_match.strip
else
f.print line
end
end
s = procinfostr(stat, tag, flag)
f.puts 'X-TMail-Status: ' + s unless s.empty?
f.puts
while s = r.read(2048)
f.write s
end
}
end
def get_status( tag )
File.foreach(@filename) {|line|
return false if line.strip.empty?
if m = /\AX-TMail-Status:/i.match(line)
return m.post_match.strip.include?(tag[0])
end
}
false
end
end
class MaildirPort < FilePort
def move_to_new
new = replace_dir(@filename, 'new')
File.rename @filename, new
@filename = new
end
def move_to_cur
new = replace_dir(@filename, 'cur')
File.rename @filename, new
@filename = new
end
def replace_dir( path, dir )
"#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}"
end
private :replace_dir
include MailFlags
private
MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/
def set_status( tag, flag )
if m = MAIL_FILE.match(File.basename(@filename))
s, uniq, type, info, = m.to_a
return if type and type != '2' # do not change anything
newname = File.dirname(@filename) + '/' +
uniq + ':2,' + procinfostr(info.to_s, tag, flag)
else
newname = @filename + ':2,' + tag
end
File.link @filename, newname
File.unlink @filename
@filename = newname
end
def get_status( tag )
m = MAIL_FILE.match(File.basename(@filename)) or return false
m[2] == '2' and m[3].to_s.include?(tag[0])
end
end
###
### StringPort
###
class StringPort < Port
def initialize( str = '' )
@buffer = str
super()
end
def string
@buffer
end
def to_s
@buffer.dup
end
alias read_all to_s
def size
@buffer.size
end
def ==( other )
StringPort === other and @buffer.equal? other.string
end
alias eql? ==
def hash
@buffer.object_id.hash
end
def inspect
"#<#{self.class}:id=#{sprintf '0x%x', @buffer.object_id}>"
end
def reproducible?
true
end
def ropen( &block )
@buffer or raise Errno::ENOENT, "#{inspect} is already removed"
StringInput.open(@buffer, &block)
end
def wopen( &block )
@buffer = ''
StringOutput.new(@buffer, &block)
end
def aopen( &block )
@buffer ||= ''
StringOutput.new(@buffer, &block)
end
def remove
@buffer = nil
end
alias rm remove
def copy_to( port )
port.wopen {|f|
f.write @buffer
}
end
alias cp copy_to
def move_to( port )
if StringPort === port
str = @buffer
port.instance_eval { @buffer = str }
else
copy_to port
end
remove
end
end
end # module TMail
=begin rdoc
= Quoting methods
=end
module TMail
class Mail
def subject(to_charset = 'utf-8')
Unquoter.unquote_and_convert_to(quoted_subject, to_charset)
end
def unquoted_body(to_charset = 'utf-8')
from_charset = sub_header("content-type", "charset")
case (content_transfer_encoding || "7bit").downcase
when "quoted-printable"
# the default charset is set to iso-8859-1 instead of 'us-ascii'.
# This is needed as many mailer do not set the charset but send in ISO. This is only used if no charset is set.
if !from_charset.blank? && from_charset.downcase == 'us-ascii'
from_charset = 'iso-8859-1'
end
Unquoter.unquote_quoted_printable_and_convert_to(quoted_body,
to_charset, from_charset, true)
when "base64"
Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset,
from_charset)
when "7bit", "8bit"
Unquoter.convert_to(quoted_body, to_charset, from_charset)
when "binary"
quoted_body
else
quoted_body
end
end
def body(to_charset = 'utf-8', &block)
attachment_presenter = block || Proc.new { |file_name| "Attachment: #{file_name}\n" }
if multipart?
parts.collect { |part|
header = part["content-type"]
if part.multipart?
part.body(to_charset, &attachment_presenter)
elsif header.nil?
""
elsif !attachment?(part)
part.unquoted_body(to_charset)
else
attachment_presenter.call(header["name"] || "(unnamed)")
end
}.join
else
unquoted_body(to_charset)
end
end
end
class Unquoter
class << self
def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false)
return "" if text.nil?
text.gsub(/(.*?)(?:(?:=\?(.*?)\?(.)\?(.*?)\?=)|$)/) do
before = $1
from_charset = $2
quoting_method = $3
text = $4
before = convert_to(before, to_charset, from_charset) if before.length > 0
before + case quoting_method
when "q", "Q" then
unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores)
when "b", "B" then
unquote_base64_and_convert_to(text, to_charset, from_charset)
when nil then
# will be nil at the end of the string, due to the nature of
# the regex used.
""
else
raise "unknown quoting method #{quoting_method.inspect}"
end
end
end
def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false)
text = text.gsub(/_/, " ") unless preserve_underscores
text = text.gsub(/\r\n|\r/, "\n") # normalize newlines
convert_to(text.unpack("M*").first, to, from)
end
def unquote_base64_and_convert_to(text, to, from)
convert_to(Base64.decode(text), to, from)
end
begin
require 'iconv'
def convert_to(text, to, from)
return text unless to && from
text ? Iconv.iconv(to, from, text).first : ""
rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
# the 'from' parameter specifies a charset other than what the text
# actually is...not much we can do in this case but just return the
# unconverted text.
#
# Ditto if either parameter represents an unknown charset, like
# X-UNKNOWN.
text
end
rescue LoadError
# Not providing quoting support
def convert_to(text, to, from)
warn "Action Mailer: iconv not loaded; ignoring conversion from #{from} to #{to} (#{__FILE__}:#{__LINE__})"
text
end
end
end
end
end
#:stopdoc:
require 'rbconfig'
# Attempts to require anative extension.
# Falls back to pure-ruby version, if it fails.
#
# This uses Config::CONFIG['arch'] from rbconfig.
def require_arch(fname)
arch = Config::CONFIG['arch']
begin
path = File.join("tmail", arch, fname)
require path
rescue LoadError => e
# try pre-built Windows binaries
if arch =~ /mswin/
require File.join("tmail", 'mswin32', fname)
else
raise e
end
end
end
# def require_arch(fname)
# dext = Config::CONFIG['DLEXT']
# begin
# if File.extname(fname) == dext
# path = fname
# else
# path = File.join("tmail","#{fname}.#{dext}")
# end
# require path
# rescue LoadError => e
# begin
# arch = Config::CONFIG['arch']
# path = File.join("tmail", arch, "#{fname}.#{dext}")
# require path
# rescue LoadError
# case path
# when /i686/
# path.sub!('i686', 'i586')
# when /i586/
# path.sub!('i586', 'i486')
# when /i486/
# path.sub!('i486', 'i386')
# else
# begin
# require fname + '.rb'
# rescue LoadError
# raise e
# end
# end
# retry
# end
# end
# end
#:startdoc:=begin rdoc
= Scanner for TMail
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
#:stopdoc:
#require 'tmail/require_arch'
require 'tmail/utils'
require 'tmail/config'
module TMail
# NOTE: It woiuld be nice if these two libs could boith be called "tmailscanner", and
# the native extension would have precedence. However RubyGems boffs that up b/c
# it does not gaurantee load_path order.
begin
raise LoadError, 'Turned off native extentions by user choice' if ENV['NORUBYEXT']
require('tmail/tmailscanner') # c extension
Scanner = TMailScanner
rescue LoadError
require 'tmail/scanner_r'
Scanner = TMailScanner
end
end
#:stopdoc:# scanner_r.rb
#
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
#:stopdoc:
require 'tmail/config'
module TMail
class TMailScanner
Version = '1.2.3'
Version.freeze
MIME_HEADERS = {
:CTYPE => true,
:CENCODING => true,
:CDISPOSITION => true
}
alnum = 'a-zA-Z0-9'
atomsyms = %q[ _#!$%&`'*+-{|}~^/=? ].strip
tokensyms = %q[ _#!$%&`'*+-{|}~^@. ].strip
atomchars = alnum + Regexp.quote(atomsyms)
tokenchars = alnum + Regexp.quote(tokensyms)
iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B'
eucstr = "(?:[\xa1-\xfe][\xa1-\xfe])+"
sjisstr = "(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+"
utf8str = "(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+"
quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n
domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n
comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n
quoted_without_iso2022 = /\A[^\\"]+/n
domlit_without_iso2022 = /\A[^\\\]]+/n
comment_without_iso2022 = /\A[^\\()]+/n
PATTERN_TABLE = {}
PATTERN_TABLE['EUC'] =
[
/\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n,
/\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n,
quoted_with_iso2022,
domlit_with_iso2022,
comment_with_iso2022
]
PATTERN_TABLE['SJIS'] =
[
/\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n,
/\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n,
quoted_with_iso2022,
domlit_with_iso2022,
comment_with_iso2022
]
PATTERN_TABLE['UTF8'] =
[
/\A(?:[#{atomchars}]+|#{utf8str})+/n,
/\A(?:[#{tokenchars}]+|#{utf8str})+/n,
quoted_without_iso2022,
domlit_without_iso2022,
comment_without_iso2022
]
PATTERN_TABLE['NONE'] =
[
/\A[#{atomchars}]+/n,
/\A[#{tokenchars}]+/n,
quoted_without_iso2022,
domlit_without_iso2022,
comment_without_iso2022
]
def initialize( str, scantype, comments )
init_scanner str
@comments = comments || []
@debug = false
# fix scanner mode
@received = (scantype == :RECEIVED)
@is_mime_header = MIME_HEADERS[scantype]
atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[TMail.KCODE]
@word_re = (MIME_HEADERS[scantype] ? token : atom)
end
attr_accessor :debug
def scan( &block )
if @debug
scan_main do |arr|
s, v = arr
printf "%7d %-10s %s\n",
rest_size(),
s.respond_to?(:id2name) ? s.id2name : s.inspect,
v.inspect
yield arr
end
else
scan_main(&block)
end
end
private
RECV_TOKEN = {
'from' => :FROM,
'by' => :BY,
'via' => :VIA,
'with' => :WITH,
'id' => :ID,
'for' => :FOR
}
def scan_main
until eof?
if skip(/\A[\n\r\t ]+/n) # LWSP
break if eof?
end
if s = readstr(@word_re)
if @is_mime_header
yield [:TOKEN, s]
else
# atom
if /\A\d+\z/ === s
yield [:DIGIT, s]
elsif @received
yield [RECV_TOKEN[s.downcase] || :ATOM, s]
else
yield [:ATOM, s]
end
end
elsif skip(/\A"/)
yield [:QUOTED, scan_quoted_word()]
elsif skip(/\A\[/)
yield [:DOMLIT, scan_domain_literal()]
elsif skip(/\A\(/)
@comments.push scan_comment()
else
c = readchar()
yield [c, c]
end
end
yield [false, '$']
end
def scan_quoted_word
scan_qstr(@quoted_re, /\A"/, 'quoted-word')
end
def scan_domain_literal
'[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']'
end
def scan_qstr( pattern, terminal, type )
result = ''
until eof?
if s = readstr(pattern) then result << s
elsif skip(terminal) then return result
elsif skip(/\A\\/) then result << readchar()
else
raise "TMail FATAL: not match in #{type}"
end
end
scan_error! "found unterminated #{type}"
end
def scan_comment
result = ''
nest = 1
content = @comment_re
until eof?
if s = readstr(content) then result << s
elsif skip(/\A\)/) then nest -= 1
return result if nest == 0
result << ')'
elsif skip(/\A\(/) then nest += 1
result << '('
elsif skip(/\A\\/) then result << readchar()
else
raise 'TMail FATAL: not match in comment'
end
end
scan_error! 'found unterminated comment'
end
# string scanner
def init_scanner( str )
@src = str
end
def eof?
@src.empty?
end
def rest_size
@src.size
end
def readstr( re )
if m = re.match(@src)
@src = m.post_match
m[0]
else
nil
end
end
def readchar
readstr(/\A./)
end
def skip( re )
if m = re.match(@src)
@src = m.post_match
true
else
false
end
end
def scan_error!( msg )
raise SyntaxError, msg
end
end
end # module TMail
#:startdoc:# encoding: utf-8
=begin rdoc
= String handling class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
class StringInput#:nodoc:
include Enumerable
class << self
def new( str )
if block_given?
begin
f = super
yield f
ensure
f.close if f
end
else
super
end
end
alias open new
end
def initialize( str )
@src = str
@pos = 0
@closed = false
@lineno = 0
end
attr_reader :lineno
def string
@src
end
def inspect
"#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@src[0,30].inspect}>"
end
def close
stream_check!
@pos = nil
@closed = true
end
def closed?
@closed
end
def pos
stream_check!
[@pos, @src.size].min
end
alias tell pos
def seek( offset, whence = IO::SEEK_SET )
stream_check!
case whence
when IO::SEEK_SET
@pos = offset
when IO::SEEK_CUR
@pos += offset
when IO::SEEK_END
@pos = @src.size - offset
else
raise ArgumentError, "unknown seek flag: #{whence}"
end
@pos = 0 if @pos < 0
@pos = [@pos, @src.size + 1].min
offset
end
def rewind
stream_check!
@pos = 0
end
def eof?
stream_check!
@pos > @src.size
end
def each( &block )
stream_check!
begin
@src.each(&block)
ensure
@pos = 0
end
end
def gets
stream_check!
if idx = @src.index(?\n, @pos)
idx += 1 # "\n".size
line = @src[ @pos ... idx ]
@pos = idx
@pos += 1 if @pos == @src.size
else
line = @src[ @pos .. -1 ]
@pos = @src.size + 1
end
@lineno += 1
line
end
def getc
stream_check!
ch = @src[@pos]
@pos += 1
@pos += 1 if @pos == @src.size
ch
end
def read( len = nil )
stream_check!
return read_all unless len
str = @src[@pos, len]
@pos += len
@pos += 1 if @pos == @src.size
str
end
alias sysread read
def read_all
stream_check!
return nil if eof?
rest = @src[@pos ... @src.size]
@pos = @src.size + 1
rest
end
def stream_check!
@closed and raise IOError, 'closed stream'
end
end
class StringOutput#:nodoc:
class << self
def new( str = '' )
if block_given?
begin
f = super
yield f
ensure
f.close if f
end
else
super
end
end
alias open new
end
def initialize( str = '' )
@dest = str
@closed = false
end
def close
@closed = true
end
def closed?
@closed
end
def string
@dest
end
alias value string
alias to_str string
def size
@dest.size
end
alias pos size
def inspect
"#<#{self.class}:#{@dest ? 'open' : 'closed'},#{object_id}>"
end
def print( *args )
stream_check!
raise ArgumentError, 'wrong # of argument (0 for >1)' if args.empty?
args.each do |s|
raise ArgumentError, 'nil not allowed' if s.nil?
@dest << s.to_s
end
nil
end
def puts( *args )
stream_check!
args.each do |str|
@dest << (s = str.to_s)
@dest << "\n" unless s[-1] == ?\n
end
@dest << "\n" if args.empty?
nil
end
def putc( ch )
stream_check!
@dest << ch.chr
nil
end
def printf( *args )
stream_check!
@dest << sprintf(*args)
nil
end
def write( str )
stream_check!
s = str.to_s
@dest << s
s.size
end
alias syswrite write
def <<( str )
stream_check!
@dest << str.to_s
self
end
private
def stream_check!
@closed and raise IOError, 'closed stream'
end
end
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
# = TMail - The EMail Swiss Army Knife for Ruby
#
# The TMail library provides you with a very complete way to handle and manipulate EMails
# from within your Ruby programs.
#
# Used as the backbone for email handling by the Ruby on Rails and Nitro web frameworks as
# well as a bunch of other Ruby apps including the Ruby-Talk mailing list to newsgroup email
# gateway, it is a proven and reliable email handler that won't let you down.
#
# Originally created by Minero Aoki, TMail has been recently picked up by Mikel Lindsaar and
# is being actively maintained. Numerous backlogged bug fixes have been applied as well as
# Ruby 1.9 compatibility and a swath of documentation to boot.
#
# TMail allows you to treat an email totally as an object and allow you to get on with your
# own programming without having to worry about crafting the perfect email address validation
# parser, or assembling an email from all it's component parts.
#
# TMail handles the most complex part of the email - the header. It generates and parses
# headers and provides you with instant access to their innards through simple and logically
# named accessor and setter methods.
#
# TMail also provides a wrapper to Net/SMTP as well as Unix Mailbox handling methods to
# directly read emails from your unix mailbox, parse them and use them.
#
# Following is the comprehensive list of methods to access TMail::Mail objects. You can also
# check out TMail::Mail, TMail::Address and TMail::Headers for other lists.
module TMail
# Provides an exception to throw on errors in Syntax within TMail's parsers
class SyntaxError < StandardError; end
# Provides a new email boundary to separate parts of the email. This is a random
# string based off the current time, so should be fairly unique.
#
# For Example:
#
# TMail.new_boundary
# #=> "mimepart_47bf656968207_25a8fbb80114"
# TMail.new_boundary
# #=> "mimepart_47bf66051de4_25a8fbb80240"
def TMail.new_boundary
'mimepart_' + random_tag
end
# Provides a new email message ID. You can use this to generate unique email message
# id's for your email so you can track them.
#
# Optionally takes a fully qualified domain name (default to the current hostname
# returned by Socket.gethostname) that will be appended to the message ID.
#
# For Example:
#
# email.message_id = TMail.new_message_id
# #=> "<[email protected]>"
# email.to_s
# #=> "Message-Id: <[email protected]>\n\n"
# email.message_id = TMail.new_message_id("lindsaar.net")
# #=> "<[email protected]>"
# email.to_s
# #=> "Message-Id: <[email protected]>\n\n"
def TMail.new_message_id( fqdn = nil )
fqdn ||= ::Socket.gethostname
"<#{random_tag()}@#{fqdn}.tmail>"
end
#:stopdoc:
def TMail.random_tag #:nodoc:
@uniq += 1
t = Time.now
sprintf('%x%x_%x%x%d%x',
t.to_i, t.tv_usec,
$$, Thread.current.object_id, @uniq, rand(255))
end
private_class_method :random_tag
@uniq = 0
#:startdoc:
# Text Utils provides a namespace to define TOKENs, ATOMs, PHRASEs and CONTROL characters that
# are OK per RFC 2822.
#
# It also provides methods you can call to determine if a string is safe
module TextUtils
aspecial = %Q|()<>[]:;.\\,"|
tspecial = %Q|()<>[];:\\,"/?=|
lwsp = %Q| \t\r\n|
control = %Q|\x00-\x1f\x7f-\xff|
CONTROL_CHAR = /[#{control}]/n
ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n
PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n
# Returns true if the string supplied is free from characters not allowed as an ATOM
def atom_safe?( str )
not ATOM_UNSAFE === str
end
# If the string supplied has ATOM unsafe characters in it, will return the string quoted
# in double quotes, otherwise returns the string unmodified
def quote_atom( str )
(ATOM_UNSAFE === str) ? dquote(str) : str
end
# If the string supplied has PHRASE unsafe characters in it, will return the string quoted
# in double quotes, otherwise returns the string unmodified
def quote_phrase( str )
(PHRASE_UNSAFE === str) ? dquote(str) : str
end
# Returns true if the string supplied is free from characters not allowed as a TOKEN
def token_safe?( str )
not TOKEN_UNSAFE === str
end
# If the string supplied has TOKEN unsafe characters in it, will return the string quoted
# in double quotes, otherwise returns the string unmodified
def quote_token( str )
(TOKEN_UNSAFE === str) ? dquote(str) : str
end
# Wraps supplied string in double quotes unless it is already wrapped
# Returns double quoted string
def dquote( str ) #:nodoc:
unless str =~ /^".*?"$/
'"' + str.gsub(/["\\]/n) {|s| '\\' + s } + '"'
else
str
end
end
private :dquote
# Unwraps supplied string from inside double quotes
# Returns unquoted string
def unquote( str )
str =~ /^"(.*?)"$/ ? $1 : str
end
# Provides a method to join a domain name by it's parts and also makes it
# ATOM safe by quoting it as needed
def join_domain( arr )
arr.map {|i|
if /\A\[.*\]\z/ === i
i
else
quote_atom(i)
end
}.join('.')
end
#:stopdoc:
ZONESTR_TABLE = {
'jst' => 9 * 60,
'eet' => 2 * 60,
'bst' => 1 * 60,
'met' => 1 * 60,
'gmt' => 0,
'utc' => 0,
'ut' => 0,
'nst' => -(3 * 60 + 30),
'ast' => -4 * 60,
'edt' => -4 * 60,
'est' => -5 * 60,
'cdt' => -5 * 60,
'cst' => -6 * 60,
'mdt' => -6 * 60,
'mst' => -7 * 60,
'pdt' => -7 * 60,
'pst' => -8 * 60,
'a' => -1 * 60,
'b' => -2 * 60,
'c' => -3 * 60,
'd' => -4 * 60,
'e' => -5 * 60,
'f' => -6 * 60,
'g' => -7 * 60,
'h' => -8 * 60,
'i' => -9 * 60,
# j not use
'k' => -10 * 60,
'l' => -11 * 60,
'm' => -12 * 60,
'n' => 1 * 60,
'o' => 2 * 60,
'p' => 3 * 60,
'q' => 4 * 60,
'r' => 5 * 60,
's' => 6 * 60,
't' => 7 * 60,
'u' => 8 * 60,
'v' => 9 * 60,
'w' => 10 * 60,
'x' => 11 * 60,
'y' => 12 * 60,
'z' => 0 * 60
}
#:startdoc:
# Takes a time zone string from an EMail and converts it to Unix Time (seconds)
def timezone_string_to_unixtime( str )
if m = /([\+\-])(\d\d?)(\d\d)/.match(str)
sec = (m[2].to_i * 60 + m[3].to_i) * 60
m[1] == '-' ? -sec : sec
else
min = ZONESTR_TABLE[str.downcase] or
raise SyntaxError, "wrong timezone format '#{str}'"
min * 60
end
end
#:stopdoc:
WDAY = %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG )
MONTH = %w( TMailBUG Jan Feb Mar Apr May Jun
Jul Aug Sep Oct Nov Dec TMailBUG )
def time2str( tm )
# [ruby-list:7928]
gmt = Time.at(tm.to_i)
gmt.gmtime
offset = tm.to_i - Time.local(*gmt.to_a[0,6].reverse).to_i
# DO NOT USE strftime: setlocale() breaks it
sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d',
WDAY[tm.wday], tm.mday, MONTH[tm.month],
tm.year, tm.hour, tm.min, tm.sec,
*(offset / 60).divmod(60)
end
MESSAGE_ID = /<[^\@>]+\@[^>\@]+>/
def message_id?( str )
MESSAGE_ID === str
end
MIME_ENCODED = /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i
def mime_encoded?( str )
MIME_ENCODED === str
end
def decode_params( hash )
new = Hash.new
encoded = nil
hash.each do |key, value|
if m = /\*(?:(\d+)\*)?\z/.match(key)
((encoded ||= {})[m.pre_match] ||= [])[(m[1] || 0).to_i] = value
else
new[key] = to_kcode(value)
end
end
if encoded
encoded.each do |key, strings|
new[key] = decode_RFC2231(strings.join(''))
end
end
new
end
NKF_FLAGS = {
'EUC' => '-e -m',
'SJIS' => '-s -m'
}
def to_kcode( str )
flag = NKF_FLAGS[TMail.KCODE] or return str
NKF.nkf(flag, str)
end
RFC2231_ENCODED = /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in
def decode_RFC2231( str )
m = RFC2231_ENCODED.match(str) or return str
begin
to_kcode(m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr })
rescue
m.post_match.gsub(/%[\da-f]{2}/in, "")
end
end
def quote_boundary
# Make sure the Content-Type boundary= parameter is quoted if it contains illegal characters
# (to ensure any special characters in the boundary text are escaped from the parser
# (such as = in MS Outlook's boundary text))
if @body =~ /^(.*)boundary=(.*)$/m
preamble = $1
remainder = $2
if remainder =~ /;/
remainder =~ /^(.*?)(;.*)$/m
boundary_text = $1
post = $2.chomp
else
boundary_text = remainder.chomp
end
if boundary_text =~ /[\/\?\=]/
boundary_text = "\"#{boundary_text}\"" unless boundary_text =~ /^".*?"$/
@body = "#{preamble}boundary=#{boundary_text}#{post}"
end
end
end
#:startdoc:
end
end
#
# version.rb
#
#--
# Copyright (c) 1998-2003 Minero Aoki <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
#:stopdoc:
module TMail
module VERSION
MAJOR = 1
MINOR = 2
TINY = 3
STRING = [MAJOR, MINOR, TINY].join('.')
end
end
require 'tmail/version'
require 'tmail/mail'
require 'tmail/mailbox'
require 'tmail/core_extensions'
require 'tmail/net'
# Prefer gems to the bundled libs.
require 'rubygems'
begin
gem 'tmail', '~> 1.2.3'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/tmail-1.2.3"
end
module TMail
end
require 'tmail'
require 'active_support/core_ext/kernel/reporting'
silence_warnings do
TMail::Encoder.const_set("MAX_LINE_LEN", 200)
end
module ActionMailer
module VERSION #:nodoc:
MAJOR = 3
MINOR = 0
TINY = "pre"
STRING = [MAJOR, MINOR, TINY].join('.')
end
end
#--
# Copyright (c) 2004-2009 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
actionpack_path = "#{File.dirname(__FILE__)}/../../actionpack/lib"
$:.unshift(actionpack_path) if File.directory?(actionpack_path)
require 'action_controller'
require 'action_view'
module ActionMailer
def self.load_all!
[Base, Part, ::Text::Format, ::Net::SMTP]
end
autoload :AdvAttrAccessor, 'action_mailer/adv_attr_accessor'
autoload :DeprecatedBody, 'action_mailer/deprecated_body'
autoload :Base, 'action_mailer/base'
autoload :DeliveryMethod, 'action_mailer/delivery_method'
autoload :Part, 'action_mailer/part'
autoload :PartContainer, 'action_mailer/part_container'
autoload :Quoting, 'action_mailer/quoting'
autoload :TestCase, 'action_mailer/test_case'
autoload :TestHelper, 'action_mailer/test_helper'
autoload :Utils, 'action_mailer/utils'
end
module Text
autoload :Format, 'action_mailer/vendor/text_format'
end
module Net
autoload :SMTP, 'net/smtp'
end
autoload :MailHelper, 'action_mailer/mail_helper'
require 'action_mailer/vendor/tmail'
root = File.expand_path('../../..', __FILE__)
begin
require "#{root}/vendor/gems/environment"
rescue LoadError
$:.unshift("#{root}/activesupport/lib")
$:.unshift("#{root}/actionpack/lib")
end
lib = File.expand_path("#{File.dirname(__FILE__)}/../lib")
$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib)
require 'rubygems'
require 'test/unit'
require 'action_mailer'
require 'action_mailer/test_case'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
# Bogus template processors
ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect }
ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect }
ActionView::Base::DEFAULT_CONFIG = { :assets_dir => '/nowhere' }
$:.unshift "#{File.dirname(__FILE__)}/fixtures/helpers"
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
ActionMailer::Base.template_root = FIXTURE_LOAD_PATH
class MockSMTP
def self.deliveries
@@deliveries
end
def initialize
@@deliveries = []
end
def sendmail(mail, from, to)
@@deliveries << [mail, from, to]
end
def start(*args)
yield self
end
end
class Net::SMTP
def self.new(*args)
MockSMTP.new
end
end
def uses_gem(gem_name, test_name, version = '> 0')
gem gem_name.to_s, version
require gem_name.to_s
yield
rescue LoadError
$stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again."
end
def set_delivery_method(delivery_method)
@old_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.delivery_method = delivery_method
end
def restore_delivery_method
ActionMailer::Base.delivery_method = @old_delivery_method
end
require 'abstract_unit'
require 'action_mailer/adv_attr_accessor'
class AdvAttrTest < Test::Unit::TestCase
class Person
include ActionMailer::AdvAttrAccessor
adv_attr_accessor :name
end
def test_adv_attr
bob = Person.new
assert_nil bob.name
bob.name 'Bob'
assert_equal 'Bob', bob.name
assert_raise(ArgumentError) {bob.name 'x', 'y'}
end
end
require 'abstract_unit'
class AssetHostMailer < ActionMailer::Base
def email_with_asset(recipient)
recipients recipient
subject "testing email containing asset path while asset_host is set"
from "[email protected]"
end
end
class AssetHostTest < Test::Unit::TestCase
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def teardown
restore_delivery_method
end
def test_asset_host_as_string
ActionController::Base.asset_host = "http://www.example.com"
mail = AssetHostMailer.deliver_email_with_asset(@recipient)
assert_equal "<img alt=\"Somelogo\" src=\"http://www.example.com/images/somelogo.png\" />", mail.body.strip
end
def test_asset_host_as_one_arguement_proc
ActionController::Base.asset_host = Proc.new { |source|
if source.starts_with?('/images')
"http://images.example.com"
else
"http://assets.example.com"
end
}
mail = AssetHostMailer.deliver_email_with_asset(@recipient)
assert_equal "<img alt=\"Somelogo\" src=\"http://images.example.com/images/somelogo.png\" />", mail.body.strip
end
def test_asset_host_as_two_arguement_proc
ActionController::Base.asset_host = Proc.new {|source,request|
if request && request.ssl?
"https://www.example.com"
else
"http://www.example.com"
end
}
mail = nil
assert_nothing_raised { mail = AssetHostMailer.deliver_email_with_asset(@recipient) }
assert_equal "<img alt=\"Somelogo\" src=\"http://www.example.com/images/somelogo.png\" />", mail.body.strip
end
endrequire 'abstract_unit'
class DefaultDeliveryMethodMailer < ActionMailer::Base
end
class NonDefaultDeliveryMethodMailer < ActionMailer::Base
self.delivery_method = :sendmail
end
class FileDeliveryMethodMailer < ActionMailer::Base
self.delivery_method = :file
end
class CustomDeliveryMethod
attr_accessor :custom_deliveries
def initialize()
@customer_deliveries = []
end
def self.perform_delivery(mail)
self.custom_deliveries << mail
end
end
class CustomerDeliveryMailer < ActionMailer::Base
self.delivery_method = CustomDeliveryMethod.new
end
class ActionMailerBase_delivery_method_Test < Test::Unit::TestCase
def setup
set_delivery_method :smtp
end
def teardown
restore_delivery_method
end
def test_should_be_the_default_smtp
assert_instance_of ActionMailer::DeliveryMethod::Smtp, ActionMailer::Base.delivery_method
end
end
class DefaultDeliveryMethodMailer_delivery_method_Test < Test::Unit::TestCase
def setup
set_delivery_method :smtp
end
def teardown
restore_delivery_method
end
def test_should_be_the_default_smtp
assert_instance_of ActionMailer::DeliveryMethod::Smtp, DefaultDeliveryMethodMailer.delivery_method
end
end
class NonDefaultDeliveryMethodMailer_delivery_method_Test < Test::Unit::TestCase
def setup
set_delivery_method :smtp
end
def teardown
restore_delivery_method
end
def test_should_be_the_set_delivery_method
assert_instance_of ActionMailer::DeliveryMethod::Sendmail, NonDefaultDeliveryMethodMailer.delivery_method
end
end
class FileDeliveryMethodMailer_delivery_method_Test < Test::Unit::TestCase
def setup
set_delivery_method :smtp
end
def teardown
restore_delivery_method
end
def test_should_be_the_set_delivery_method
assert_instance_of ActionMailer::DeliveryMethod::File, FileDeliveryMethodMailer.delivery_method
end
def test_should_default_location_to_the_tmpdir
assert_equal "#{Dir.tmpdir}/mails", ActionMailer::Base.file_settings[:location]
end
end
class CustomDeliveryMethodMailer_delivery_method_Test < Test::Unit::TestCase
def setup
set_delivery_method :smtp
end
def teardown
restore_delivery_method
end
def test_should_be_the_set_delivery_method
assert_instance_of CustomDeliveryMethod, CustomerDeliveryMailer.delivery_method
end
end
module ExampleHelper
def example_format(text)
"<em><strong><small>#{h(text)}</small></strong></em>".html_safe!
end
end
require 'abstract_unit'
module MailerHelper
def person_name
"Mr. Joe Person"
end
end
class HelperMailer < ActionMailer::Base
helper MailerHelper
helper :example
def use_helper(recipient)
recipients recipient
subject "using helpers"
from "[email protected]"
end
def use_example_helper(recipient)
recipients recipient
subject "using helpers"
from "[email protected]"
@text = "emphasize me!"
end
def use_mail_helper(recipient)
recipients recipient
subject "using mailing helpers"
from "[email protected]"
@text = "But soft! What light through yonder window breaks? It is the east, " +
"and Juliet is the sun. Arise, fair sun, and kill the envious moon, " +
"which is sick and pale with grief that thou, her maid, art far more " +
"fair than she. Be not her maid, for she is envious! Her vestal " +
"livery is but sick and green, and none but fools do wear it. Cast " +
"it off!"
end
def use_helper_method(recipient)
recipients recipient
subject "using helpers"
from "[email protected]"
@text = "emphasize me!"
end
private
def name_of_the_mailer_class
self.class.name
end
helper_method :name_of_the_mailer_class
end
class MailerHelperTest < Test::Unit::TestCase
def new_mail( charset="utf-8" )
mail = TMail::Mail.new
mail.set_content_type "text", "plain", { "charset" => charset } if charset
mail
end
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def teardown
restore_delivery_method
end
def test_use_helper
mail = HelperMailer.create_use_helper(@recipient)
assert_match %r{Mr. Joe Person}, mail.encoded
end
def test_use_example_helper
mail = HelperMailer.create_use_example_helper(@recipient)
assert_match %r{<em><strong><small>emphasize me!}, mail.encoded
end
def test_use_helper_method
mail = HelperMailer.create_use_helper_method(@recipient)
assert_match %r{HelperMailer}, mail.encoded
end
def test_use_mail_helper
mail = HelperMailer.create_use_mail_helper(@recipient)
assert_match %r{ But soft!}, mail.encoded
assert_match %r{east, and\n Juliet}, mail.encoded
end
end
require 'abstract_unit'
class AutoLayoutMailer < ActionMailer::Base
def hello(recipient)
recipients recipient
subject "You have a mail"
from "[email protected]"
end
def spam(recipient)
recipients recipient
subject "You have a mail"
from "[email protected]"
@world = "Earth"
render(:inline => "Hello, <%= @world %>", :layout => 'spam')
end
def nolayout(recipient)
recipients recipient
subject "You have a mail"
from "[email protected]"
@world = "Earth"
render(:inline => "Hello, <%= @world %>", :layout => false)
end
def multipart(recipient, type = nil)
recipients recipient
subject "You have a mail"
from "[email protected]"
content_type(type) if type
end
end
class ExplicitLayoutMailer < ActionMailer::Base
layout 'spam', :except => [:logout]
def signup(recipient)
recipients recipient
subject "You have a mail"
from "[email protected]"
end
def logout(recipient)
recipients recipient
subject "You have a mail"
from "[email protected]"
end
end
class LayoutMailerTest < Test::Unit::TestCase
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def teardown
restore_delivery_method
end
def test_should_pickup_default_layout
mail = AutoLayoutMailer.create_hello(@recipient)
assert_equal "Hello from layout Inside", mail.body.strip
end
def test_should_pickup_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient)
assert_equal "multipart/alternative", mail.content_type
assert_equal 2, mail.parts.size
assert_equal 'text/plain', mail.parts.first.content_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
end
def test_should_pickup_multipartmixed_layout
mail = AutoLayoutMailer.create_multipart(@recipient, "multipart/mixed")
assert_equal "multipart/mixed", mail.content_type
assert_equal 2, mail.parts.size
assert_equal 'text/plain', mail.parts.first.content_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
end
def test_should_fix_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient, "text/plain")
assert_equal "multipart/alternative", mail.content_type
assert_equal 2, mail.parts.size
assert_equal 'text/plain', mail.parts.first.content_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
end
def test_should_pickup_layout_given_to_render
mail = AutoLayoutMailer.create_spam(@recipient)
assert_equal "Spammer layout Hello, Earth", mail.body.strip
end
def test_should_respect_layout_false
mail = AutoLayoutMailer.create_nolayout(@recipient)
assert_equal "Hello, Earth", mail.body.strip
end
def test_explicit_class_layout
mail = ExplicitLayoutMailer.create_signup(@recipient)
assert_equal "Spammer layout We do not spam", mail.body.strip
end
def test_explicit_layout_exceptions
mail = ExplicitLayoutMailer.create_logout(@recipient)
assert_equal "You logged out", mail.body.strip
end
end
require 'abstract_unit'
class RenderMailer < ActionMailer::Base
def inline_template(recipient)
recipients recipient
subject "using helpers"
from "[email protected]"
@world = "Earth"
render :inline => "Hello, <%= @world %>"
end
def file_template(recipient)
recipients recipient
subject "using helpers"
from "[email protected]"
@recipient = recipient
render :file => "templates/signed_up"
end
def implicit_body(recipient)
recipients recipient
subject "using helpers"
from "[email protected]"
@recipient = recipient
render :template => "templates/signed_up"
end
def rxml_template(recipient)
recipients recipient
subject "rendering rxml template"
from "[email protected]"
end
def included_subtemplate(recipient)
recipients recipient
subject "Including another template in the one being rendered"
from "[email protected]"
end
def included_old_subtemplate(recipient)
recipients recipient
subject "Including another template in the one being rendered"
from "[email protected]"
@world = "Earth"
render :inline => "Hello, <%= render \"subtemplate\" %>"
end
def initialize_defaults(method_name)
super
mailer_name "test_mailer"
end
end
class FirstMailer < ActionMailer::Base
def share(recipient)
recipients recipient
subject "using helpers"
from "[email protected]"
end
end
class SecondMailer < ActionMailer::Base
def share(recipient)
recipients recipient
subject "using helpers"
from "[email protected]"
end
end
class RenderHelperTest < Test::Unit::TestCase
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def teardown
restore_delivery_method
end
def test_implicit_body
mail = RenderMailer.create_implicit_body(@recipient)
assert_equal "Hello there, \n\nMr. test@localhost", mail.body.strip
end
def test_inline_template
mail = RenderMailer.create_inline_template(@recipient)
assert_equal "Hello, Earth", mail.body.strip
end
def test_file_template
mail = RenderMailer.create_file_template(@recipient)
assert_equal "Hello there, \n\nMr. test@localhost", mail.body.strip
end
def test_rxml_template
mail = RenderMailer.deliver_rxml_template(@recipient)
assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test/>", mail.body.strip
end
def test_included_subtemplate
mail = RenderMailer.deliver_included_subtemplate(@recipient)
assert_equal "Hey Ho, let's go!", mail.body.strip
end
end
class FirstSecondHelperTest < Test::Unit::TestCase
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def teardown
restore_delivery_method
end
def test_ordering
mail = FirstMailer.create_share(@recipient)
assert_equal "first mail", mail.body.strip
mail = SecondMailer.create_share(@recipient)
assert_equal "second mail", mail.body.strip
mail = FirstMailer.create_share(@recipient)
assert_equal "first mail", mail.body.strip
mail = SecondMailer.create_share(@recipient)
assert_equal "second mail", mail.body.strip
end
end
# encoding: utf-8
require 'abstract_unit'
class FunkyPathMailer < ActionMailer::Base
self.template_root = "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
def multipart_with_template_path_with_dots(recipient)
recipients recipient
subject "This path has dots"
from "Chad Fowler <[email protected]>"
attachment :content_type => "text/plain",
:body => "dots dots dots..."
end
end
class TestMailer < ActionMailer::Base
def signed_up(recipient)
recipients recipient
subject "[Signed up] Welcome #{recipient}"
from "[email protected]"
@recipient = recipient
end
def cancelled_account(recipient)
recipients recipient
subject "[Cancelled] Goodbye #{recipient}"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
render :text => "Goodbye, Mr. #{recipient}"
end
def cc_bcc(recipient)
recipients recipient
subject "testing bcc/cc"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
cc "[email protected]"
bcc "[email protected]"
render :text => "Nothing to see here."
end
def different_reply_to(recipient)
recipients recipient
subject "testing reply_to"
from "[email protected]"
sent_on Time.local(2008, 5, 23)
reply_to "[email protected]"
render :text => "Nothing to see here."
end
def iso_charset(recipient)
recipients recipient
subject "testing isø charsets"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
cc "[email protected]"
bcc "[email protected]"
charset "iso-8859-1"
render :text => "Nothing to see here."
end
def unencoded_subject(recipient)
recipients recipient
subject "testing unencoded subject"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
cc "[email protected]"
bcc "[email protected]"
render :text => "Nothing to see here."
end
def extended_headers(recipient)
recipients recipient
subject "testing extended headers"
from "Grytøyr <[email protected]>"
sent_on Time.local(2004, 12, 12)
cc "Grytøyr <[email protected]>"
bcc "Grytøyr <[email protected]>"
charset "iso-8859-1"
render :text => "Nothing to see here."
end
def utf8_body(recipient)
recipients recipient
subject "testing utf-8 body"
from "Foo áëô îü <[email protected]>"
sent_on Time.local(2004, 12, 12)
cc "Foo áëô îü <[email protected]>"
bcc "Foo áëô îü <[email protected]>"
charset "utf-8"
render :text => "åœö blah"
end
def multipart_with_mime_version(recipient)
recipients recipient
subject "multipart with mime_version"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
mime_version "1.1"
content_type "multipart/alternative"
part "text/plain" do |p|
p.body = "blah"
end
part "text/html" do |p|
p.body = "<b>blah</b>"
end
end
def multipart_with_utf8_subject(recipient)
recipients recipient
subject "Foo áëô îü"
from "[email protected]"
charset "utf-8"
part "text/plain" do |p|
p.body = "blah"
end
part "text/html" do |p|
p.body = "<b>blah</b>"
end
end
def explicitly_multipart_example(recipient, ct=nil)
recipients recipient
subject "multipart example"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
content_type ct if ct
part "text/html" do |p|
p.charset = "iso-8859-1"
p.body = "blah"
end
attachment :content_type => "image/jpeg", :filename => "foo.jpg",
:body => "123456789"
render :text => "plain text default"
end
def implicitly_multipart_example(recipient, cs = nil, order = nil)
recipients recipient
subject "multipart example"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
@charset = cs if cs
@recipient = recipient
@implicit_parts_order = order if order
end
def implicitly_multipart_with_utf8
recipients "[email protected]"
subject "Foo áëô îü"
from "[email protected]"
template "implicitly_multipart_example"
@recipient = "[email protected]"
end
def html_mail(recipient)
recipients recipient
subject "html mail"
from "[email protected]"
content_type "text/html"
render :text => "<em>Emphasize</em> <strong>this</strong>"
end
def html_mail_with_underscores(recipient)
subject "html mail with underscores"
render :text => %{<a href="http://google.com" target="_blank">_Google</a>}
end
def custom_template(recipient)
recipients recipient
subject "[Signed up] Welcome #{recipient}"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
template "signed_up"
@recipient = recipient
end
def custom_templating_extension(recipient)
recipients recipient
subject "[Signed up] Welcome #{recipient}"
from "[email protected]"
sent_on Time.local(2004, 12, 12)
@recipient = recipient
end
def various_newlines(recipient)
recipients recipient
subject "various newlines"
from "[email protected]"
render :text => "line #1\nline #2\rline #3\r\nline #4\r\r" +
"line #5\n\nline#6\r\n\r\nline #7"
end
def various_newlines_multipart(recipient)
recipients recipient
subject "various newlines multipart"
from "[email protected]"
content_type "multipart/alternative"
part :content_type => "text/plain", :body => "line #1\nline #2\rline #3\r\nline #4\r\r"
part :content_type => "text/html", :body => "<p>line #1</p>\n<p>line #2</p>\r<p>line #3</p>\r\n<p>line #4</p>\r\r"
end
def nested_multipart(recipient)
recipients recipient
subject "nested multipart"
from "[email protected]"
content_type "multipart/mixed"
part :content_type => "multipart/alternative", :content_disposition => "inline", :headers => { "foo" => "bar" } 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
attachment :content_type => "application/octet-stream",:filename => "test.txt", :body => "test abcdefghijklmnopqstuvwxyz"
end
def nested_multipart_with_body(recipient)
recipients recipient
subject "nested multipart with body"
from "[email protected]"
content_type "multipart/mixed"
part :content_type => "multipart/alternative", :content_disposition => "inline", :body => "Nothing to see here." do |p|
p.part :content_type => "text/html", :body => "<b>test</b> HTML<br/>"
end
end
def attachment_with_custom_header(recipient)
recipients recipient
subject "custom header in attachment"
from "[email protected]"
content_type "multipart/related"
part :content_type => "text/html", :body => 'yo'
attachment :content_type => "image/jpeg",:filename => "test.jpeg", :body => "i am not a real picture", :headers => { 'Content-ID' => '<[email protected]>' }
end
def unnamed_attachment(recipient)
recipients recipient
subject "nested multipart"
from "[email protected]"
content_type "multipart/mixed"
part :content_type => "text/plain", :body => "hullo"
attachment :content_type => "application/octet-stream", :body => "test abcdefghijklmnopqstuvwxyz"
end
def headers_with_nonalpha_chars(recipient)
recipients recipient
subject "nonalpha chars"
from "One: Two <[email protected]>"
cc "Three: Four <[email protected]>"
bcc "Five: Six <[email protected]>"
render :text => "testing"
end
def custom_content_type_attributes
recipients "[email protected]"
subject "custom content types"
from "[email protected]"
content_type "text/plain; format=flowed"
render :text => "testing"
end
def return_path
recipients "[email protected]"
subject "return path test"
from "[email protected]"
headers "return-path" => "[email protected]"
render :text => "testing"
end
def body_ivar(recipient)
recipients recipient
subject "Body as a local variable"
from "[email protected]"
body :body => "foo", :bar => "baz"
end
def subject_with_i18n(recipient)
recipients recipient
from "[email protected]"
render :text => "testing"
end
class << self
attr_accessor :received_body
end
def receive(mail)
self.class.received_body = mail.body
end
end
class ActionMailerTest < Test::Unit::TestCase
include ActionMailer::Quoting
def encode( text, charset="utf-8" )
quoted_printable( text, charset )
end
def new_mail( charset="utf-8" )
mail = TMail::Mail.new
mail.mime_version = "1.0"
if charset
mail.set_content_type "text", "plain", { "charset" => charset }
end
mail
end
# Replacing logger work around for mocha bug. Should be fixed in mocha 0.3.3
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.raise_delivery_errors = true
ActionMailer::Base.deliveries = []
@original_logger = TestMailer.logger
@recipient = 'test@localhost'
end
def teardown
TestMailer.logger = @original_logger
restore_delivery_method
end
def test_nested_parts
created = nil
assert_nothing_raised { created = TestMailer.create_nested_multipart(@recipient)}
assert_equal 2,created.parts.size
assert_equal 2,created.parts.first.parts.size
assert_equal "multipart/mixed", created.content_type
assert_equal "multipart/alternative", created.parts.first.content_type
assert_equal "bar", created.parts.first.header['foo'].to_s
assert_nil created.parts.first.charset
assert_equal "text/plain", created.parts.first.parts.first.content_type
assert_equal "text/html", created.parts.first.parts[1].content_type
assert_equal "application/octet-stream", created.parts[1].content_type
end
def test_nested_parts_with_body
created = nil
TestMailer.create_nested_multipart_with_body(@recipient)
assert_nothing_raised { created = TestMailer.create_nested_multipart_with_body(@recipient)}
assert_equal 1,created.parts.size
assert_equal 2,created.parts.first.parts.size
assert_equal "multipart/mixed", created.content_type
assert_equal "multipart/alternative", created.parts.first.content_type
assert_equal "Nothing to see here.", created.parts.first.parts.first.body
assert_equal "text/plain", created.parts.first.parts.first.content_type
assert_equal "text/html", created.parts.first.parts[1].content_type
end
def test_attachment_with_custom_header
created = nil
assert_nothing_raised { created = TestMailer.create_attachment_with_custom_header(@recipient) }
assert created.parts.any? { |p| p.header['content-id'].to_s == "<[email protected]>" }
end
def test_signed_up
Time.stubs(:now => Time.now)
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "[email protected]"
expected.date = Time.now
created = nil
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_signed_up(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_subject_with_i18n
assert_nothing_raised { TestMailer.deliver_subject_with_i18n(@recipient) }
assert_equal "Subject with i18n", ActionMailer::Base.deliveries.first.subject
I18n.backend.store_translations('en', :actionmailer => {:test_mailer => {:subject_with_i18n => {:subject => "New Subject!"}}})
assert_nothing_raised { TestMailer.deliver_subject_with_i18n(@recipient) }
assert_equal "New Subject!", ActionMailer::Base.deliveries.last.subject
end
def test_custom_template
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "[email protected]"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_custom_template(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
end
def test_custom_templating_extension
assert ActionView::Template.template_handler_extensions.include?("haml"), "haml extension was not registered"
# N.b., custom_templating_extension.text.plain.haml is expected to be in fixtures/test_mailer directory
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "[email protected]"
expected.date = Time.local(2004, 12, 12)
# Stub the render method so no alternative renderers need be present.
ActionView::Base.any_instance.stubs(:render).returns("Hello there, \n\nMr. #{@recipient}")
# Now that the template is registered, there should be one part. The text/plain part.
created = nil
assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) }
assert_not_nil created
assert_equal 2, created.parts.length
assert_equal 'text/plain', created.parts[0].content_type
assert_equal 'text/html', created.parts[1].content_type
end
def test_cancelled_account
expected = new_mail
expected.to = @recipient
expected.subject = "[Cancelled] Goodbye #{@recipient}"
expected.body = "Goodbye, Mr. #{@recipient}"
expected.from = "[email protected]"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_cancelled_account(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_cancelled_account(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_cc_bcc
expected = new_mail
expected.to = @recipient
expected.subject = "testing bcc/cc"
expected.body = "Nothing to see here."
expected.from = "[email protected]"
expected.cc = "[email protected]"
expected.bcc = "[email protected]"
expected.date = Time.local 2004, 12, 12
created = nil
assert_nothing_raised do
created = TestMailer.create_cc_bcc @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_cc_bcc @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_reply_to
expected = new_mail
expected.to = @recipient
expected.subject = "testing reply_to"
expected.body = "Nothing to see here."
expected.from = "[email protected]"
expected.reply_to = "[email protected]"
expected.date = Time.local 2008, 5, 23
created = nil
assert_nothing_raised do
created = TestMailer.create_different_reply_to @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_different_reply_to @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_iso_charset
expected = new_mail( "iso-8859-1" )
expected.to = @recipient
expected.subject = encode "testing isø charsets", "iso-8859-1"
expected.body = "Nothing to see here."
expected.from = "[email protected]"
expected.cc = "[email protected]"
expected.bcc = "[email protected]"
expected.date = Time.local 2004, 12, 12
created = nil
assert_nothing_raised do
created = TestMailer.create_iso_charset @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_iso_charset @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_unencoded_subject
expected = new_mail
expected.to = @recipient
expected.subject = "testing unencoded subject"
expected.body = "Nothing to see here."
expected.from = "[email protected]"
expected.cc = "[email protected]"
expected.bcc = "[email protected]"
expected.date = Time.local 2004, 12, 12
created = nil
assert_nothing_raised do
created = TestMailer.create_unencoded_subject @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_unencoded_subject @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_instances_are_nil
assert_nil ActionMailer::Base.new
assert_nil TestMailer.new
end
def test_deliveries_array
assert_not_nil ActionMailer::Base.deliveries
assert_equal 0, ActionMailer::Base.deliveries.size
TestMailer.deliver_signed_up(@recipient)
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not_nil ActionMailer::Base.deliveries.first
end
def test_perform_deliveries_flag
ActionMailer::Base.perform_deliveries = false
TestMailer.deliver_signed_up(@recipient)
assert_equal 0, ActionMailer::Base.deliveries.size
ActionMailer::Base.perform_deliveries = true
TestMailer.deliver_signed_up(@recipient)
assert_equal 1, ActionMailer::Base.deliveries.size
end
def test_doesnt_raise_errors_when_raise_delivery_errors_is_false
ActionMailer::Base.raise_delivery_errors = false
TestMailer.delivery_method.expects(:perform_delivery).raises(Exception)
assert_nothing_raised { TestMailer.deliver_signed_up(@recipient) }
end
def test_performs_delivery_via_sendmail
sm = mock()
sm.expects(:print).with(anything)
sm.expects(:flush)
IO.expects(:popen).once.with('/usr/sbin/sendmail -i -t', 'w+').yields(sm)
ActionMailer::Base.delivery_method = :sendmail
TestMailer.deliver_signed_up(@recipient)
end
class FakeLogger
attr_reader :info_contents, :debug_contents
def initialize
@info_contents, @debug_contents = "", ""
end
def info(str = nil, &blk)
@info_contents << str if str
@info_contents << blk.call if block_given?
end
def debug(str = nil, &blk)
@debug_contents << str if str
@debug_contents << blk.call if block_given?
end
end
def test_delivery_logs_sent_mail
mail = TestMailer.create_signed_up(@recipient)
# logger = mock()
# logger.expects(:info).with("Sent mail to #{@recipient}")
# logger.expects(:debug).with("\n#{mail.encoded}")
TestMailer.logger = FakeLogger.new
TestMailer.deliver_signed_up(@recipient)
assert(TestMailer.logger.info_contents =~ /Sent mail to #{@recipient}/)
assert_equal(TestMailer.logger.debug_contents, "\n#{mail.encoded}")
end
def test_unquote_quoted_printable_subject
msg = <<EOF
From: [email protected]
Subject: =?utf-8?Q?testing_testing_=D6=A4?=
Content-Type: text/plain; charset=iso-8859-1
The body
EOF
mail = TMail::Mail.parse(msg)
assert_equal "testing testing \326\244", mail.subject
assert_equal "=?utf-8?Q?testing_testing_=D6=A4?=", mail.quoted_subject
end
def test_unquote_7bit_subject
msg = <<EOF
From: [email protected]
Subject: this == working?
Content-Type: text/plain; charset=iso-8859-1
The body
EOF
mail = TMail::Mail.parse(msg)
assert_equal "this == working?", mail.subject
assert_equal "this == working?", mail.quoted_subject
end
def test_unquote_7bit_body
msg = <<EOF
From: [email protected]
Subject: subject
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: 7bit
The=3Dbody
EOF
mail = TMail::Mail.parse(msg)
assert_equal "The=3Dbody", mail.body.strip
assert_equal "The=3Dbody", mail.quoted_body.strip
end
def test_unquote_quoted_printable_body
msg = <<EOF
From: [email protected]
Subject: subject
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable
The=3Dbody
EOF
mail = TMail::Mail.parse(msg)
assert_equal "The=body", mail.body.strip
assert_equal "The=3Dbody", mail.quoted_body.strip
end
def test_unquote_base64_body
msg = <<EOF
From: [email protected]
Subject: subject
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: base64
VGhlIGJvZHk=
EOF
mail = TMail::Mail.parse(msg)
assert_equal "The body", mail.body.strip
assert_equal "VGhlIGJvZHk=", mail.quoted_body.strip
end
def test_extended_headers
@recipient = "Grytøyr <test@localhost>"
expected = new_mail "iso-8859-1"
expected.to = quote_address_if_necessary @recipient, "iso-8859-1"
expected.subject = "testing extended headers"
expected.body = "Nothing to see here."
expected.from = quote_address_if_necessary "Grytøyr <[email protected]>", "iso-8859-1"
expected.cc = quote_address_if_necessary "Grytøyr <[email protected]>", "iso-8859-1"
expected.bcc = quote_address_if_necessary "Grytøyr <[email protected]>", "iso-8859-1"
expected.date = Time.local 2004, 12, 12
created = nil
assert_nothing_raised do
created = TestMailer.create_extended_headers @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_extended_headers @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_utf8_body_is_not_quoted
@recipient = "Foo áëô îü <[email protected]>"
expected = new_mail "utf-8"
expected.to = quote_address_if_necessary @recipient, "utf-8"
expected.subject = "testing utf-8 body"
expected.body = "åœö blah"
expected.from = quote_address_if_necessary @recipient, "utf-8"
expected.cc = quote_address_if_necessary @recipient, "utf-8"
expected.bcc = quote_address_if_necessary @recipient, "utf-8"
expected.date = Time.local 2004, 12, 12
created = TestMailer.create_utf8_body @recipient
assert_match(/åœö blah/, created.encoded)
end
def test_multiple_utf8_recipients
@recipient = ["\"Foo áëô îü\" <[email protected]>", "\"Example Recipient\" <[email protected]>"]
expected = new_mail "utf-8"
expected.to = quote_address_if_necessary @recipient, "utf-8"
expected.subject = "testing utf-8 body"
expected.body = "åœö blah"
expected.from = quote_address_if_necessary @recipient.first, "utf-8"
expected.cc = quote_address_if_necessary @recipient, "utf-8"
expected.bcc = quote_address_if_necessary @recipient, "utf-8"
expected.date = Time.local 2004, 12, 12
created = TestMailer.create_utf8_body @recipient
assert_match(/\nFrom: =\?utf-8\?Q\?Foo_.*?\?= <[email protected]>\r/, created.encoded)
assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= <[email protected]>, Example Recipient <me/, created.encoded)
end
def test_receive_decodes_base64_encoded_mail
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email")
TestMailer.receive(fixture)
assert_match(/Jamis/, TestMailer.received_body)
end
def test_receive_attachments
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email2")
mail = TMail::Mail.parse(fixture)
attachment = mail.attachments.last
assert_equal "smime.p7s", attachment.original_filename
assert_equal "application/pkcs7-signature", attachment.content_type
end
def test_decode_attachment_without_charset
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email3")
mail = TMail::Mail.parse(fixture)
attachment = mail.attachments.last
assert_equal 1026, attachment.read.length
end
def test_attachment_using_content_location
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email12")
mail = TMail::Mail.parse(fixture)
assert_equal 1, mail.attachments.length
assert_equal "Photo25.jpg", mail.attachments.first.original_filename
end
def test_attachment_with_text_type
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email13")
mail = TMail::Mail.parse(fixture)
assert mail.has_attachments?
assert_equal 1, mail.attachments.length
assert_equal "hello.rb", mail.attachments.first.original_filename
end
def test_decode_part_without_content_type
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email4")
mail = TMail::Mail.parse(fixture)
assert_nothing_raised { mail.body }
end
def test_decode_message_without_content_type
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email5")
mail = TMail::Mail.parse(fixture)
assert_nothing_raised { mail.body }
end
def test_decode_message_with_incorrect_charset
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email6")
mail = TMail::Mail.parse(fixture)
assert_nothing_raised { mail.body }
end
def test_multipart_with_mime_version
mail = TestMailer.create_multipart_with_mime_version(@recipient)
assert_equal "1.1", mail.mime_version
end
def test_multipart_with_utf8_subject
mail = TestMailer.create_multipart_with_utf8_subject(@recipient)
assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded)
end
def test_implicitly_multipart_with_utf8
mail = TestMailer.create_implicitly_multipart_with_utf8
assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded)
end
def test_explicitly_multipart_messages
mail = TestMailer.create_explicitly_multipart_example(@recipient)
assert_equal 3, mail.parts.length
assert_nil mail.content_type
assert_equal "text/plain", mail.parts[0].content_type
assert_equal "text/html", mail.parts[1].content_type
assert_equal "iso-8859-1", mail.parts[1].sub_header("content-type", "charset")
assert_equal "inline", mail.parts[1].content_disposition
assert_equal "image/jpeg", mail.parts[2].content_type
assert_equal "attachment", mail.parts[2].content_disposition
assert_equal "foo.jpg", mail.parts[2].sub_header("content-disposition", "filename")
assert_equal "foo.jpg", mail.parts[2].sub_header("content-type", "name")
assert_nil mail.parts[2].sub_header("content-type", "charset")
end
def test_explicitly_multipart_with_content_type
mail = TestMailer.create_explicitly_multipart_example(@recipient, "multipart/alternative")
assert_equal 3, mail.parts.length
assert_equal "multipart/alternative", mail.content_type
end
def test_explicitly_multipart_with_invalid_content_type
mail = TestMailer.create_explicitly_multipart_example(@recipient, "text/xml")
assert_equal 3, mail.parts.length
assert_nil mail.content_type
end
def test_implicitly_multipart_messages
assert ActionView::Template.template_handler_extensions.include?("bak"), "bak extension was not registered"
mail = TestMailer.create_implicitly_multipart_example(@recipient)
assert_equal 3, mail.parts.length
assert_equal "1.0", mail.mime_version
assert_equal "multipart/alternative", mail.content_type
assert_equal "application/x-yaml", mail.parts[0].content_type
assert_equal "utf-8", mail.parts[0].sub_header("content-type", "charset")
assert_equal "text/plain", mail.parts[1].content_type
assert_equal "utf-8", mail.parts[1].sub_header("content-type", "charset")
assert_equal "text/html", mail.parts[2].content_type
assert_equal "utf-8", mail.parts[2].sub_header("content-type", "charset")
end
def test_implicitly_multipart_messages_with_custom_order
assert ActionView::Template.template_handler_extensions.include?("bak"), "bak extension was not registered"
mail = TestMailer.create_implicitly_multipart_example(@recipient, nil, ["application/x-yaml", "text/plain"])
assert_equal 3, mail.parts.length
assert_equal "text/html", mail.parts[0].content_type
assert_equal "text/plain", mail.parts[1].content_type
assert_equal "application/x-yaml", mail.parts[2].content_type
end
def test_implicitly_multipart_messages_with_charset
mail = TestMailer.create_implicitly_multipart_example(@recipient, 'iso-8859-1')
assert_equal "multipart/alternative", mail.header['content-type'].body
assert_equal 'iso-8859-1', mail.parts[0].sub_header("content-type", "charset")
assert_equal 'iso-8859-1', mail.parts[1].sub_header("content-type", "charset")
assert_equal 'iso-8859-1', mail.parts[2].sub_header("content-type", "charset")
end
def test_html_mail
mail = TestMailer.create_html_mail(@recipient)
assert_equal "text/html", mail.content_type
end
def test_html_mail_with_underscores
mail = TestMailer.create_html_mail_with_underscores(@recipient)
assert_equal %{<a href="http://google.com" target="_blank">_Google</a>}, mail.body
end
def test_various_newlines
mail = TestMailer.create_various_newlines(@recipient)
assert_equal("line #1\nline #2\nline #3\nline #4\n\n" +
"line #5\n\nline#6\n\nline #7", mail.body)
end
def test_various_newlines_multipart
mail = TestMailer.create_various_newlines_multipart(@recipient)
assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body
assert_equal "<p>line #1</p>\n<p>line #2</p>\n<p>line #3</p>\n<p>line #4</p>\n\n", mail.parts[1].body
end
def test_headers_removed_on_smtp_delivery
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_cc_bcc(@recipient)
assert MockSMTP.deliveries[0][2].include?("[email protected]")
assert MockSMTP.deliveries[0][2].include?("[email protected]")
assert MockSMTP.deliveries[0][2].include?(@recipient)
assert_match %r{^Cc: [email protected]}, MockSMTP.deliveries[0][0]
assert_match %r{^To: #{@recipient}}, MockSMTP.deliveries[0][0]
assert_no_match %r{^Bcc: [email protected]}, MockSMTP.deliveries[0][0]
end
def test_file_delivery_should_create_a_file
ActionMailer::Base.delivery_method = :file
tmp_location = ActionMailer::Base.file_settings[:location]
TestMailer.deliver_cc_bcc(@recipient)
assert File.exists?(tmp_location)
assert File.directory?(tmp_location)
assert File.exists?(File.join(tmp_location, @recipient))
assert File.exists?(File.join(tmp_location, '[email protected]'))
assert File.exists?(File.join(tmp_location, '[email protected]'))
end
def test_recursive_multipart_processing
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email7")
mail = TMail::Mail.parse(fixture)
assert_equal "This is the first part.\n\nAttachment: test.rb\nAttachment: test.pdf\n\n\nAttachment: smime.p7s\n", mail.body
end
def test_decode_encoded_attachment_filename
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email8")
mail = TMail::Mail.parse(fixture)
attachment = mail.attachments.last
expected = "01 Quien Te Dij\212at. Pitbull.mp3"
expected.force_encoding(Encoding::ASCII_8BIT) if expected.respond_to?(:force_encoding)
assert_equal expected, attachment.original_filename
end
def test_wrong_mail_header
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email9")
assert_raise(TMail::SyntaxError) { TMail::Mail.parse(fixture) }
end
def test_decode_message_with_unknown_charset
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email10")
mail = TMail::Mail.parse(fixture)
assert_nothing_raised { mail.body }
end
def test_empty_header_values_omitted
result = TestMailer.create_unnamed_attachment(@recipient).encoded
assert_match %r{Content-Type: application/octet-stream[^;]}, result
assert_match %r{Content-Disposition: attachment[^;]}, result
end
def test_headers_with_nonalpha_chars
mail = TestMailer.create_headers_with_nonalpha_chars(@recipient)
assert !mail.from_addrs.empty?
assert !mail.cc_addrs.empty?
assert !mail.bcc_addrs.empty?
assert_match(/:/, mail.from_addrs.to_s)
assert_match(/:/, mail.cc_addrs.to_s)
assert_match(/:/, mail.bcc_addrs.to_s)
end
def test_deliver_with_mail_object
mail = TestMailer.create_headers_with_nonalpha_chars(@recipient)
assert_nothing_raised { TestMailer.deliver(mail) }
assert_equal 1, TestMailer.deliveries.length
end
def test_multipart_with_template_path_with_dots
mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient)
assert_equal 2, mail.parts.length
assert "text/plain", mail.parts[1].content_type
assert "utf-8", mail.parts[1].charset
end
def test_custom_content_type_attributes
mail = TestMailer.create_custom_content_type_attributes
assert_match %r{format=flowed}, mail['content-type'].to_s
assert_match %r{charset=utf-8}, mail['content-type'].to_s
end
def test_return_path_with_create
mail = TestMailer.create_return_path
assert_equal "<[email protected]>", mail['return-path'].to_s
end
def test_return_path_with_deliver
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_return_path
assert_match %r{^Return-Path: <[email protected]>}, MockSMTP.deliveries[0][0]
assert_equal "[email protected]", MockSMTP.deliveries[0][1].to_s
end
def test_body_is_stored_as_an_ivar
mail = TestMailer.create_body_ivar(@recipient)
assert_equal "body: foo\nbar: baz", mail.body
end
def test_starttls_is_enabled_if_supported
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = true
MockSMTP.any_instance.expects(:respond_to?).with(:enable_starttls_auto).returns(true)
MockSMTP.any_instance.expects(:enable_starttls_auto)
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_signed_up(@recipient)
end
def test_starttls_is_disabled_if_not_supported
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = true
MockSMTP.any_instance.expects(:respond_to?).with(:enable_starttls_auto).returns(false)
MockSMTP.any_instance.expects(:enable_starttls_auto).never
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_signed_up(@recipient)
end
def test_starttls_is_not_enabled
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = false
MockSMTP.any_instance.expects(:respond_to?).never
MockSMTP.any_instance.expects(:enable_starttls_auto).never
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_signed_up(@recipient)
ensure
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = true
end
end
class InheritableTemplateRootTest < Test::Unit::TestCase
def test_attr
expected = File.expand_path("#{File.dirname(__FILE__)}/fixtures/path.with.dots")
assert_equal expected, FunkyPathMailer.template_root.to_s
sub = Class.new(FunkyPathMailer)
sub.template_root = 'test/path'
assert_equal File.expand_path('test/path'), sub.template_root.to_s
assert_equal expected, FunkyPathMailer.template_root.to_s
end
end
class MethodNamingTest < Test::Unit::TestCase
class TestMailer < ActionMailer::Base
def send
render :text => 'foo'
end
end
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
end
def teardown
restore_delivery_method
end
def test_send_method
assert_nothing_raised do
assert_emails 1 do
TestMailer.deliver_send
end
end
end
end
class RespondToTest < Test::Unit::TestCase
class RespondToMailer < ActionMailer::Base; end
def setup
set_delivery_method :test
end
def teardown
restore_delivery_method
end
def test_should_respond_to_new
assert RespondToMailer.respond_to?(:new)
end
def test_should_respond_to_create_with_template_suffix
assert RespondToMailer.respond_to?(:create_any_old_template)
end
def test_should_respond_to_deliver_with_template_suffix
assert RespondToMailer.respond_to?(:deliver_any_old_template)
end
def test_should_not_respond_to_new_with_template_suffix
assert !RespondToMailer.respond_to?(:new_any_old_template)
end
def test_should_not_respond_to_create_with_template_suffix_unless_it_is_separated_by_an_underscore
assert !RespondToMailer.respond_to?(:createany_old_template)
end
def test_should_not_respond_to_deliver_with_template_suffix_unless_it_is_separated_by_an_underscore
assert !RespondToMailer.respond_to?(:deliverany_old_template)
end
def test_should_not_respond_to_create_with_template_suffix_if_it_begins_with_a_uppercase_letter
assert !RespondToMailer.respond_to?(:create_Any_old_template)
end
def test_should_not_respond_to_deliver_with_template_suffix_if_it_begins_with_a_uppercase_letter
assert !RespondToMailer.respond_to?(:deliver_Any_old_template)
end
def test_should_not_respond_to_create_with_template_suffix_if_it_begins_with_a_digit
assert !RespondToMailer.respond_to?(:create_1_template)
end
def test_should_not_respond_to_deliver_with_template_suffix_if_it_begins_with_a_digit
assert !RespondToMailer.respond_to?(:deliver_1_template)
end
def test_should_not_respond_to_method_where_deliver_is_not_a_suffix
assert !RespondToMailer.respond_to?(:foo_deliver_template)
end
def test_should_still_raise_exception_with_expected_message_when_calling_an_undefined_method
error = assert_raise NoMethodError do
RespondToMailer.not_a_method
end
assert_match /undefined method.*not_a_method/, error.message
end
end
# encoding: utf-8
require 'abstract_unit'
require 'tempfile'
class QuotingTest < Test::Unit::TestCase
# Move some tests from TMAIL here
def test_unquote_quoted_printable
a ="=?ISO-8859-1?Q?[166417]_Bekr=E6ftelse_fra_Rejsefeber?="
b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b
end
def test_unquote_base64
a ="=?ISO-8859-1?B?WzE2NjQxN10gQmVrcuZmdGVsc2UgZnJhIFJlanNlZmViZXI=?="
b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b
end
def test_unquote_without_charset
a ="[166417]_Bekr=E6ftelse_fra_Rejsefeber"
b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
assert_equal "[166417]_Bekr=E6ftelse_fra_Rejsefeber", b
end
def test_unqoute_multiple
a ="=?utf-8?q?Re=3A_=5B12=5D_=23137=3A_Inkonsistente_verwendung_von_=22Hin?==?utf-8?b?enVmw7xnZW4i?="
b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
assert_equal "Re: [12] #137: Inkonsistente verwendung von \"Hinzuf\303\274gen\"", b
end
def test_unqoute_in_the_middle
a ="Re: Photos =?ISO-8859-1?Q?Brosch=FCre_Rand?="
b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
assert_equal "Re: Photos Brosch\303\274re Rand", b
end
def test_unqoute_iso
a ="=?ISO-8859-1?Q?Brosch=FCre_Rand?="
b = TMail::Unquoter.unquote_and_convert_to(a, 'iso-8859-1')
expected = "Brosch\374re Rand"
expected.force_encoding 'iso-8859-1' if expected.respond_to?(:force_encoding)
assert_equal expected, b
end
def test_quote_multibyte_chars
original = "\303\246 \303\270 and \303\245"
original.force_encoding('ASCII-8BIT') if original.respond_to?(:force_encoding)
result = execute_in_sandbox(<<-CODE)
$:.unshift(File.dirname(__FILE__) + "/../lib/")
if RUBY_VERSION < '1.9'
$KCODE = 'u'
require 'jcode'
end
require 'action_mailer/quoting'
include ActionMailer::Quoting
quoted_printable(#{original.inspect}, "UTF-8")
CODE
unquoted = TMail::Unquoter.unquote_and_convert_to(result, nil)
assert_equal unquoted, original
end
# test an email that has been created using \r\n newlines, instead of
# \n newlines.
def test_email_quoted_with_0d0a
mail = TMail::Mail.parse(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_quoted_with_0d0a"))
assert_match %r{Elapsed time}, mail.body
end
def test_email_with_partially_quoted_subject
mail = TMail::Mail.parse(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_partially_quoted_subject"))
assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject
end
private
# This whole thing *could* be much simpler, but I don't think Tempfile,
# popen and others exist on all platforms (like Windows).
def execute_in_sandbox(code)
test_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.rb"
res_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.out"
File.open(test_name, "w+") do |file|
file.write(<<-CODE)
block = Proc.new do
#{code}
end
puts block.call
CODE
end
system("ruby #{test_name} > #{res_name}") or raise "could not run test in sandbox"
File.read(res_name).chomp
ensure
File.delete(test_name) rescue nil
File.delete(res_name) rescue nil
end
end
require 'abstract_unit'
class TestHelperMailer < ActionMailer::Base
def test
recipients "[email protected]"
from "[email protected]"
@world = "Earth"
render(:inline => "Hello, <%= @world %>")
end
end
class TestHelperMailerTest < ActionMailer::TestCase
def test_setup_sets_right_action_mailer_options
assert_instance_of ActionMailer::DeliveryMethod::Test, ActionMailer::Base.delivery_method
assert ActionMailer::Base.perform_deliveries
assert_equal [], ActionMailer::Base.deliveries
end
def test_setup_creates_the_expected_mailer
assert @expected.is_a?(TMail::Mail)
assert_equal "1.0", @expected.mime_version
assert_equal "text/plain", @expected.content_type
end
def test_mailer_class_is_correctly_inferred
assert_equal TestHelperMailer, self.class.mailer_class
end
def test_determine_default_mailer_raises_correct_error
assert_raise(ActionMailer::NonInferrableMailerError) do
self.class.determine_default_mailer("NotAMailerTest")
end
end
def test_charset_is_utf_8
assert_equal "utf-8", charset
end
def test_encode
assert_equal "=?utf-8?Q?=0Aasdf=0A?=", encode("\nasdf\n")
end
def test_assert_emails
assert_nothing_raised do
assert_emails 1 do
TestHelperMailer.deliver_test
end
end
end
def test_repeated_assert_emails_calls
assert_nothing_raised do
assert_emails 1 do
TestHelperMailer.deliver_test
end
end
assert_nothing_raised do
assert_emails 2 do
TestHelperMailer.deliver_test
TestHelperMailer.deliver_test
end
end
end
def test_assert_emails_with_no_block
assert_nothing_raised do
TestHelperMailer.deliver_test
assert_emails 1
end
assert_nothing_raised do
TestHelperMailer.deliver_test
TestHelperMailer.deliver_test
assert_emails 3
end
end
def test_assert_no_emails
assert_nothing_raised do
assert_no_emails do
TestHelperMailer.create_test
end
end
end
def test_assert_emails_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 2 do
TestHelperMailer.deliver_test
end
end
assert_match /2 .* but 1/, error.message
end
def test_assert_emails_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 1 do
TestHelperMailer.deliver_test
TestHelperMailer.deliver_test
end
end
assert_match /1 .* but 2/, error.message
end
def test_assert_no_emails_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_emails do
TestHelperMailer.deliver_test
end
end
assert_match /0 .* but 1/, error.message
end
end
class AnotherTestHelperMailerTest < ActionMailer::TestCase
tests TestHelperMailer
def setup
@test_var = "a value"
end
def test_setup_shouldnt_conflict_with_mailer_setup
assert @expected.is_a?(TMail::Mail)
assert_equal 'a value', @test_var
end
end
require 'abstract_unit'
class TMailMailTest < Test::Unit::TestCase
def test_body
m = TMail::Mail.new
expected = 'something_with_underscores'
m.encoding = 'quoted-printable'
quoted_body = [expected].pack('*M')
m.body = quoted_body
assert_equal "something_with_underscores=\n", m.quoted_body
assert_equal expected, m.body
end
def test_nested_attachments_are_recognized_correctly
fixture = File.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_nested_attachment")
mail = TMail::Mail.parse(fixture)
assert_equal 2, mail.attachments.length
assert_equal "image/png", mail.attachments.first.content_type
assert_equal 1902, mail.attachments.first.length
assert_equal "application/pkcs7-signature", mail.attachments.last.content_type
end
end
require 'abstract_unit'
class TestMailer < ActionMailer::Base
default_url_options[:host] = 'www.basecamphq.com'
def signed_up_with_url(recipient)
@recipients = recipient
@subject = "[Signed up] Welcome #{recipient}"
@from = "[email protected]"
@sent_on = Time.local(2004, 12, 12)
@body["recipient"] = recipient
@body["welcome_url"] = url_for :host => "example.com", :controller => "welcome", :action => "greeting"
end
class <<self
attr_accessor :received_body
end
def receive(mail)
self.class.received_body = mail.body
end
end
class ActionMailerUrlTest < Test::Unit::TestCase
include ActionMailer::Quoting
def encode( text, charset="utf-8" )
quoted_printable( text, charset )
end
def new_mail( charset="utf-8" )
mail = TMail::Mail.new
mail.mime_version = "1.0"
if charset
mail.set_content_type "text", "plain", { "charset" => charset }
end
mail
end
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def teardown
restore_delivery_method
end
def test_signed_up_with_url
ActionController::Routing.use_controllers! ['welcome'] do
ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:action/:id'
map.welcome 'welcome', :controller=>"foo", :action=>"bar"
end
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n<img alt=\"Somelogo\" src=\"/images/somelogo.png\" />"
expected.from = "[email protected]"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_signed_up_with_url(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_signed_up_with_url(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
end
end
require 'rbconfig'
require 'find'
require 'ftools'
include Config
# this was adapted from rdoc's install.rb by way of Log4r
$sitedir = CONFIG["sitelibdir"]
unless $sitedir
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
$libdir = File.join(CONFIG["libdir"], "ruby", version)
$sitedir = $:.find {|x| x =~ /site_ruby/ }
if !$sitedir
$sitedir = File.join($libdir, "site_ruby")
elsif $sitedir !~ Regexp.quote(version)
$sitedir = File.join($sitedir, version)
end
end
# the actual gruntwork
Dir.chdir("lib")
Find.find("action_controller", "action_controller.rb", "action_view", "action_view.rb") { |f|
if f[-3..-1] == ".rb"
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
else
File::makedirs(File.join($sitedir, *f.split(/\//)))
end
}module AbstractController
class Base
attr_internal :response_body
attr_internal :action_name
class << self
attr_reader :abstract
alias_method :abstract?, :abstract
# Define a controller as abstract. See internal_methods for more
# details.
def abstract!
@abstract = true
end
def inherited(klass)
::AbstractController::Base.descendants << klass.to_s
super
end
# A list of all descendents of AbstractController::Base. This is
# useful for initializers which need to add behavior to all controllers.
def descendants
@descendants ||= []
end
# A list of all internal methods for a controller. This finds the first
# abstract superclass of a controller, and gets a list of all public
# instance methods on that abstract class. Public instance methods of
# a controller would normally be considered action methods, so we
# are removing those methods on classes declared as abstract
# (ActionController::Metal and ActionController::Base are defined
# as abstract)
def internal_methods
controller = self
controller = controller.superclass until controller.abstract?
controller.public_instance_methods(true)
end
# The list of hidden actions to an empty Array. Defaults to an
# empty Array. This can be modified by other modules or subclasses
# to specify particular actions as hidden.
#
# ==== Returns
# Array[String]:: An array of method names that should not be
# considered actions.
def hidden_actions
[]
end
# A list of method names that should be considered actions. This
# includes all public instance methods on a controller, less
# any internal methods (see #internal_methods), adding back in
# any methods that are internal, but still exist on the class
# itself. Finally, #hidden_actions are removed.
#
# ==== Returns
# Array[String]:: A list of all methods that should be considered
# actions.
def action_methods
@action_methods ||=
# All public instance methods of this class, including ancestors
public_instance_methods(true).map { |m| m.to_s }.to_set -
# Except for public instance methods of Base and its ancestors
internal_methods.map { |m| m.to_s } +
# Be sure to include shadowed public instance methods of this class
public_instance_methods(false).map { |m| m.to_s } -
# And always exclude explicitly hidden actions
hidden_actions
end
end
abstract!
# Calls the action going through the entire action dispatch stack.
#
# The actual method that is called is determined by calling
# #method_for_action. If no method can handle the action, then an
# ActionNotFound error is raised.
#
# ==== Returns
# self
def process(action)
@_action_name = action_name = action.to_s
unless action_name = method_for_action(action_name)
raise ActionNotFound, "The action '#{action}' could not be found"
end
process_action(action_name)
end
private
# Returns true if the name can be considered an action. This can
# be overridden in subclasses to modify the semantics of what
# can be considered an action.
#
# ==== Parameters
# name<String>:: The name of an action to be tested
#
# ==== Returns
# TrueClass, FalseClass
def action_method?(name)
self.class.action_methods.include?(name)
end
# Call the action. Override this in a subclass to modify the
# behavior around processing an action. This, and not #process,
# is the intended way to override action dispatching.
def process_action(method_name)
send_action(method_name)
end
# Actually call the method associated with the action. Override
# this method if you wish to change how action methods are called,
# not to add additional behavior around it. For example, you would
# override #send_action if you want to inject arguments into the
# method.
alias send_action send
# If the action name was not found, but a method called "action_missing"
# was found, #method_for_action will return "_handle_action_missing".
# This method calls #action_missing with the current action name.
def _handle_action_missing
action_missing(@_action_name)
end
# Takes an action name and returns the name of the method that will
# handle the action. In normal cases, this method returns the same
# name as it receives. By default, if #method_for_action receives
# a name that is not an action, it will look for an #action_missing
# method and return "_handle_action_missing" if one is found.
#
# Subclasses may override this method to add additional conditions
# that should be considered an action. For instance, an HTTP controller
# with a template matching the action name is considered to exist.
#
# If you override this method to handle additional cases, you may
# also provide a method (like _handle_method_missing) to handle
# the case.
#
# If none of these conditions are true, and method_for_action
# returns nil, an ActionNotFound exception will be raised.
#
# ==== Parameters
# action_name<String>:: An action name to find a method name for
#
# ==== Returns
# String:: The name of the method that handles the action
# nil:: No method name could be found. Raise ActionNotFound.
def method_for_action(action_name)
if action_method?(action_name) then action_name
elsif respond_to?(:action_missing, true) then "_handle_action_missing"
end
end
end
end
module AbstractController
module Callbacks
extend ActiveSupport::Concern
# Uses ActiveSupport::Callbacks as the base functionality. For
# more details on the whole callback system, read the documentation
# for ActiveSupport::Callbacks.
include ActiveSupport::Callbacks
included do
define_callbacks :process_action, :terminator => "response_body"
end
# Override AbstractController::Base's process_action to run the
# process_action callbacks around the normal behavior.
def process_action(method_name)
run_callbacks(:process_action, method_name) do
super
end
end
module ClassMethods
# If :only or :accept are used, convert the options into the
# primitive form (:per_key) used by ActiveSupport::Callbacks.
# The basic idea is that :only => :index gets converted to
# :if => proc {|c| c.action_name == "index" }, but that the
# proc is only evaluated once per action for the lifetime of
# a Rails process.
#
# ==== Options
# :only<#to_s>:: The callback should be run only for this action
# :except<#to_s>:: The callback should be run for all actions
# except this action
def _normalize_callback_options(options)
if only = options[:only]
only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ")
options[:per_key] = {:if => only}
end
if except = options[:except]
except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ")
options[:per_key] = {:unless => except}
end
end
# Skip before, after, and around filters matching any of the names
#
# ==== Parameters
# *names<Object>:: A list of valid names that could be used for
# callbacks. Note that skipping uses Ruby equality, so it's
# impossible to skip a callback defined using an anonymous proc
# using #skip_filter
def skip_filter(*names, &blk)
skip_before_filter(*names)
skip_after_filter(*names)
skip_around_filter(*names)
end
# Take callback names and an optional callback proc, normalize them,
# then call the block with each callback. This allows us to abstract
# the normalization across several methods that use it.
#
# ==== Parameters
# callbacks<Array[*Object, Hash]>:: A list of callbacks, with an optional
# options hash as the last parameter.
# block<Proc>:: A proc that should be added to the callbacks.
#
# ==== Block Parameters
# name<Symbol>:: The callback to be added
# options<Hash>:: A list of options to be used when adding the callback
def _insert_callbacks(callbacks, block)
options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
_normalize_callback_options(options)
callbacks.push(block) if block
callbacks.each do |callback|
yield callback, options
end
end
# set up before_filter, prepend_before_filter, skip_before_filter, etc.
# for each of before, after, and around.
[:before, :after, :around].each do |filter|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
# Append a before, after or around filter. See _insert_callbacks
# for details on the allowed parameters.
def #{filter}_filter(*names, &blk)
_insert_callbacks(names, blk) do |name, options|
set_callback(:process_action, :#{filter}, name, options)
end
end
# Prepend a before, after or around filter. See _insert_callbacks
# for details on the allowed parameters.
def prepend_#{filter}_filter(*names, &blk)
_insert_callbacks(names, blk) do |name, options|
set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true))
end
end
# Skip a before, after or around filter. See _insert_callbacks
# for details on the allowed parameters.
def skip_#{filter}_filter(*names, &blk)
_insert_callbacks(names, blk) do |name, options|
skip_callback(:process_action, :#{filter}, name, options)
end
end
# *_filter is the same as append_*_filter
alias_method :append_#{filter}_filter, :#{filter}_filter
RUBY_EVAL
end
end
end
end
module AbstractController
class Error < StandardError; end
class ActionNotFound < StandardError; end
class DoubleRenderError < Error
DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"."
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
end
require 'active_support/dependencies'
module AbstractController
module Helpers
extend ActiveSupport::Concern
include RenderingController
def self.next_serial
@helper_serial ||= 0
@helper_serial += 1
end
included do
extlib_inheritable_accessor(:_helpers) { Module.new }
extlib_inheritable_accessor(:_helper_serial) do
AbstractController::Helpers.next_serial
end
end
module ClassMethods
# When a class is inherited, wrap its helper module in a new module.
# This ensures that the parent class's module can be changed
# independently of the child class's.
def inherited(klass)
helpers = _helpers
klass._helpers = Module.new { include helpers }
super
end
# Declare a controller method as a helper. For example, the following
# makes the +current_user+ controller method available to the view:
# class ApplicationController < ActionController::Base
# helper_method :current_user, :logged_in?
#
# def current_user
# @current_user ||= User.find_by_id(session[:user])
# end
#
# def logged_in?
# current_user != nil
# end
# end
#
# In a view:
# <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
#
# ==== Parameters
# meths<Array[#to_s]>:: The name of a method on the controller
# to be made available on the view.
def helper_method(*meths)
meths.flatten.each do |meth|
_helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
def #{meth}(*args, &blk)
controller.send(%(#{meth}), *args, &blk)
end
ruby_eval
end
end
# The +helper+ class method can take a series of helper module names, a block, or both.
#
# ==== Parameters
# *args<Array[Module, Symbol, String, :all]>
# block<Block>:: A block defining helper methods
#
# ==== Examples
# When the argument is a module it will be included directly in the template class.
# helper FooHelper # => includes FooHelper
#
# When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file
# and include the module in the template class. The second form illustrates how to include custom helpers
# when working with namespaced controllers, or other cases where the file containing the helper definition is not
# in one of Rails' standard load paths:
# helper :foo # => requires 'foo_helper' and includes FooHelper
# helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper
#
# Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available
# to the template.
#
# # One line
# helper { def hello() "Hello, world!" end }
#
# # Multi-line
# helper do
# def foo(bar)
# "#{bar} is the very best"
# end
# end
#
# Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of
# +symbols+, +strings+, +modules+ and blocks.
#
# helper(:three, BlindHelper) { def mice() 'mice' end }
#
def helper(*args, &block)
self._helper_serial = AbstractController::Helpers.next_serial + 1
_modules_for_helpers(args).each do |mod|
add_template_helper(mod)
end
_helpers.module_eval(&block) if block_given?
end
private
# Makes all the (instance) methods in the helper module available to templates
# rendered through this controller.
#
# ==== Parameters
# mod<Module>:: The module to include into the current helper module
# for the class
def add_template_helper(mod)
_helpers.module_eval { include mod }
end
# Returns a list of modules, normalized from the acceptable kinds of
# helpers with the following behavior:
#
# String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper",
# and "foo_bar_helper.rb" is loaded using require_dependency.
#
# Module:: No further processing
#
# After loading the appropriate files, the corresponding modules
# are returned.
#
# ==== Parameters
# args<Array[String, Symbol, Module]>:: A list of helpers
#
# ==== Returns
# Array[Module]:: A normalized list of modules for the list of
# helpers provided.
def _modules_for_helpers(args)
args.flatten.map! do |arg|
case arg
when String, Symbol
file_name = "#{arg.to_s.underscore}_helper"
require_dependency(file_name, "Missing helper file helpers/%s.rb")
file_name.camelize.constantize
when Module
arg
else
raise ArgumentError, "helper must be a String, Symbol, or Module"
end
end
end
end
end
end
module AbstractController
module Layouts
extend ActiveSupport::Concern
include RenderingController
included do
extlib_inheritable_accessor(:_layout_conditions) { Hash.new }
extlib_inheritable_accessor(:_action_has_layout) { Hash.new }
_write_layout_method
end
module ClassMethods
def inherited(klass)
super
klass.class_eval do
_write_layout_method
@found_layouts = {}
end
end
def clear_template_caches!
@found_layouts.clear if @found_layouts
super
end
def cache_layout(details)
layout = @found_layouts
key = Thread.current[:format_locale_key]
# Cache nil
if layout.key?(key)
return layout[key]
else
layout[key] = yield
end
end
# This module is mixed in if layout conditions are provided. This means
# that if no layout conditions are used, this method is not used
module LayoutConditions
# Determines whether the current action has a layout by checking the
# action name against the :only and :except conditions set on the
# layout.
#
# ==== Returns
# Boolean:: True if the action has a layout, false otherwise.
def _action_has_layout?
conditions = _layout_conditions
if only = conditions[:only]
only.include?(action_name)
elsif except = conditions[:except]
!except.include?(action_name)
else
true
end
end
end
# Specify the layout to use for this class.
#
# If the specified layout is a:
# String:: the String is the template name
# Symbol:: call the method specified by the symbol, which will return
# the template name
# false:: There is no layout
# true:: raise an ArgumentError
#
# ==== Parameters
# layout<String, Symbol, false)>:: The layout to use.
#
# ==== Options (conditions)
# :only<#to_s, Array[#to_s]>:: A list of actions to apply this layout to.
# :except<#to_s, Array[#to_s]>:: Apply this layout to all actions but this one
def layout(layout, conditions = {})
include LayoutConditions unless conditions.empty?
conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} }
self._layout_conditions = conditions
@_layout = layout || false # Converts nil to false
_write_layout_method
end
# If no layout is supplied, look for a template named the return
# value of this method.
#
# ==== Returns
# String:: A template name
def _implied_layout_name
name && name.underscore
end
# Takes the specified layout and creates a _layout method to be called
# by _default_layout
#
# If there is no explicit layout specified:
# If a layout is found in the view paths with the controller's
# name, return that string. Otherwise, use the superclass'
# layout (which might also be implied)
def _write_layout_method
case defined?(@_layout) ? @_layout : nil
when String
self.class_eval %{def _layout(details) #{@_layout.inspect} end}
when Symbol
self.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
def _layout(details)
#{@_layout}.tap do |layout|
unless layout.is_a?(String) || !layout
raise ArgumentError, "Your layout method :#{@_layout} returned \#{layout}. It " \
"should have returned a String, false, or nil"
end
end
end
ruby_eval
when false
self.class_eval %{def _layout(details) end}
when true
raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil"
when nil
if name
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def _layout(details)
self.class.cache_layout(details) do
if template_exists?("#{_implied_layout_name}", details, :_prefix => "layouts")
"#{_implied_layout_name}"
else
super
end
end
end
RUBY
end
end
self.class_eval { private :_layout }
end
end
def render_to_body(options = {})
# In the case of a partial with a layout, handle the layout
# here, and make sure the view does not try to handle it
layout = options.delete(:layout) if options.key?(:partial)
response = super
# This is a little bit messy. We need to explicitly handle partial
# layouts here since the core lookup logic is in the view, but
# we need to determine the layout based on the controller
#
# TODO: An easier way to handle this would probably be to override
# render_template
if layout
layout = _layout_for_option(layout, options[:_template].details)
response = layout.render(view_context, options[:locals] || {}) { response }
end
response
end
private
# This will be overwritten by _write_layout_method
def _layout(details) end
# Determine the layout for a given name and details.
#
# ==== Parameters
# name<String>:: The name of the template
# details<Hash{Symbol => Object}>:: A list of details to restrict
# the lookup to. By default, layout lookup is limited to the
# formats specified for the current request.
def _layout_for_name(name, details)
name && _find_layout(name, details)
end
# Determine the layout for a given name and details, taking into account
# the name type.
#
# ==== Parameters
# name<String|TrueClass|FalseClass|Symbol>:: The name of the template
# details<Hash{Symbol => Object}>:: A list of details to restrict
# the lookup to. By default, layout lookup is limited to the
# formats specified for the current request.
def _layout_for_option(name, details)
case name
when String then _layout_for_name(name, details)
when true then _default_layout(details, true)
when :default then _default_layout(details, false)
when false, nil then nil
else
raise ArgumentError,
"String, true, or false, expected for `layout'; you passed #{name.inspect}"
end
end
def _determine_template(options)
super
return unless (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout)
layout = options.key?(:layout) ? options[:layout] : :default
options[:_layout] = _layout_for_option(layout, options[:_template].details)
end
# Take in the name and details and find a Template.
#
# ==== Parameters
# name<String>:: The name of the template to retrieve
# details<Hash>:: A list of details to restrict the search by. This
# might include details like the format or locale of the template.
#
# ==== Returns
# Template:: A template object matching the name and details
def _find_layout(name, details)
# TODO: Make prefix actually part of details in ViewPath#find_by_parts
prefix = details.key?(:prefix) ? details.delete(:prefix) : "layouts"
find_template(name, details, :_prefix => prefix)
end
# Returns the default layout for this controller and a given set of details.
# Optionally raises an exception if the layout could not be found.
#
# ==== Parameters
# details<Hash>:: A list of details to restrict the search by. This
# might include details like the format or locale of the template.
# require_layout<Boolean>:: If this is true, raise an ArgumentError
# with details about the fact that the exception could not be
# found (defaults to false)
#
# ==== Returns
# Template:: The template object for the default layout (or nil)
def _default_layout(details, require_layout = false)
if require_layout && _action_has_layout? && !_layout(details)
raise ArgumentError,
"There was no default layout for #{self.class} in #{view_paths.inspect}"
end
begin
_layout_for_name(_layout(details), details) if _action_has_layout?
rescue NameError => e
raise NoMethodError,
"You specified #{@_layout.inspect} as the layout, but no such method was found"
end
end
def _action_has_layout?
true
end
end
end
module AbstractController
class HashKey
@hash_keys = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = {} } }
def self.get(klass, formats, locale)
@hash_keys[klass][formats][locale] ||= new(klass, formats, locale)
end
attr_accessor :hash
def initialize(klass, formats, locale)
@formats, @locale = formats, locale
@hash = [formats, locale].hash
end
alias_method :eql?, :equal?
def inspect
"#<HashKey -- formats: #{@formats.inspect} locale: #{@locale.inspect}>"
end
end
module LocalizedCache
extend ActiveSupport::Concern
module ClassMethods
def clear_template_caches!
ActionView::Partials::PartialRenderer::TEMPLATES.clear
template_cache.clear
super
end
def template_cache
@template_cache ||= Hash.new {|h,k| h[k] = {} }
end
end
def render(options)
Thread.current[:format_locale_key] = HashKey.get(self.class, formats, I18n.locale)
super
end
private
def with_template_cache(name)
self.class.template_cache[Thread.current[:format_locale_key]][name] ||= super
end
end
end
require 'active_support/core_ext/logger'
require 'active_support/benchmarkable'
module AbstractController
module Logger
extend ActiveSupport::Concern
included do
cattr_accessor :logger
extend ActiveSupport::Benchmarkable
end
# A class that allows you to defer expensive processing
# until the logger actually tries to log. Otherwise, you are
# forced to do the processing in advance, and send the
# entire processed String to the logger, which might
# just discard the String if the log level is too low.
#
# TODO: Require that Rails loggers accept a block.
class DelayedLog < ActiveSupport::BasicObject
def initialize(&block)
@str, @block = nil, block
end
def method_missing(*args, &block)
unless @str
@str, @block = @block.call, nil
end
@str.send(*args, &block)
end
end
# Override process_action in the AbstractController::Base
# to log details about the method.
def process_action(action)
result = ActiveSupport::Notifications.instrument(:process_action,
:controller => self, :action => action) do
super
end
if logger
log = DelayedLog.new do
"\n\nProcessing #{self.class.name}\##{action_name} " \
"to #{request.formats} (for #{request_origin}) " \
"[#{request.method.to_s.upcase}]"
end
logger.info(log)
end
result
end
private
# Returns the request origin with the IP and time. This needs to be cached,
# otherwise we would get different results for each time it calls.
def request_origin
@request_origin ||= "#{request.remote_ip} at #{Time.now.to_s(:db)}"
end
end
end
require "abstract_controller/logger"
module AbstractController
module RenderingController
extend ActiveSupport::Concern
include AbstractController::Logger
included do
attr_internal :formats
extlib_inheritable_accessor :_view_paths
self._view_paths ||= ActionView::PathSet.new
end
# Initialize controller with nil formats.
def initialize(*) #:nodoc:
@_formats = nil
super
end
# An instance of a view class. The default view class is ActionView::Base
#
# The view class must have the following methods:
# View.for_controller[controller] Create a new ActionView instance for a
# controller
# View#render_partial[options]
# - responsible for setting options[:_template]
# - Returns String with the rendered partial
# options<Hash>:: see _render_partial in ActionView::Base
# View#render_template[template, layout, options, partial]
# - Returns String with the rendered template
# template<ActionView::Template>:: The template to render
# layout<ActionView::Template>:: The layout to render around the template
# options<Hash>:: See _render_template_with_layout in ActionView::Base
# partial<Boolean>:: Whether or not the template to render is a partial
#
# Override this method in a to change the default behavior.
def view_context
@_view_context ||= ActionView::Base.for_controller(self)
end
# Mostly abstracts the fact that calling render twice is a DoubleRenderError.
# Delegates render_to_body and sticks the result in self.response_body.
def render(*args)
if response_body
raise AbstractController::DoubleRenderError, "OMG"
end
self.response_body = render_to_body(*args)
end
# Raw rendering of a template to a Rack-compatible body.
#
# ==== Options
# _partial_object<Object>:: The object that is being rendered. If this
# exists, we are in the special case of rendering an object as a partial.
#
# :api: plugin
def render_to_body(options = {})
# TODO: Refactor so we can just use the normal template logic for this
if options.key?(:partial)
view_context.render_partial(options)
else
_determine_template(options)
_render_template(options)
end
end
# Raw rendering of a template to a string. Just convert the results of
# render_to_body into a String.
#
# :api: plugin
def render_to_string(options = {})
AbstractController::RenderingController.body_to_s(render_to_body(options))
end
# Renders the template from an object.
#
# ==== Options
# _template<ActionView::Template>:: The template to render
# _layout<ActionView::Template>:: The layout to wrap the template in (optional)
# _partial<TrueClass, FalseClass>:: Whether or not the template to be rendered is a partial
def _render_template(options)
view_context.render_template(options)
end
# The list of view paths for this controller. See ActionView::ViewPathSet for
# more details about writing custom view paths.
def view_paths
_view_paths
end
# Return a string representation of a Rack-compatible response body.
def self.body_to_s(body)
if body.respond_to?(:to_str)
body
else
strings = []
body.each { |part| strings << part.to_s }
body.close if body.respond_to?(:close)
strings.join
end
end
private
# Take in a set of options and determine the template to render
#
# ==== Options
# _template<ActionView::Template>:: If this is provided, the search is over
# _template_name<#to_s>:: The name of the template to look up. Otherwise,
# use the current action name.
# _prefix<String>:: The prefix to look inside of. In a file system, this corresponds
# to a directory.
# _partial<TrueClass, FalseClass>:: Whether or not the file to look up is a partial
def _determine_template(options)
if options.key?(:text)
options[:_template] = ActionView::TextTemplate.new(options[:text], format_for_text)
elsif options.key?(:inline)
handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb")
template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {})
options[:_template] = template
elsif options.key?(:template)
options[:_template_name] = options[:template]
elsif options.key?(:file)
options[:_template_name] = options[:file]
end
name = (options[:_template_name] || action_name).to_s
options[:_template] ||= with_template_cache(name) do
find_template(name, { :formats => formats }, options)
end
end
def find_template(name, details, options)
view_paths.find(name, details, options[:_prefix], options[:_partial])
end
def template_exists?(name, details, options)
view_paths.exists?(name, details, options[:_prefix], options[:_partial])
end
def with_template_cache(name)
yield
end
def format_for_text
Mime[:text]
end
module ClassMethods
def clear_template_caches!
end
# Append a path to the list of view paths for this controller.
#
# ==== Parameters
# path<String, ViewPath>:: If a String is provided, it gets converted into
# the default view path. You may also provide a custom view path
# (see ActionView::ViewPathSet for more information)
def append_view_path(path)
self.view_paths << path
end
# Prepend a path to the list of view paths for this controller.
#
# ==== Parameters
# path<String, ViewPath>:: If a String is provided, it gets converted into
# the default view path. You may also provide a custom view path
# (see ActionView::ViewPathSet for more information)
def prepend_view_path(path)
clear_template_caches!
self.view_paths.unshift(path)
end
# A list of all of the default view paths for this controller.
def view_paths
self._view_paths
end
# Set the view paths.
#
# ==== Parameters
# paths<ViewPathSet, Object>:: If a ViewPathSet is provided, use that;
# otherwise, process the parameter into a ViewPathSet.
def view_paths=(paths)
clear_template_caches!
self._view_paths = paths.is_a?(ActionView::PathSet) ?
paths : ActionView::Base.process_view_paths(paths)
end
end
end
end
require "active_support/core_ext/module/attr_internal"
require "active_support/core_ext/module/delegation"
module AbstractController
autoload :Base, "abstract_controller/base"
autoload :Callbacks, "abstract_controller/callbacks"
autoload :Helpers, "abstract_controller/helpers"
autoload :Layouts, "abstract_controller/layouts"
autoload :LocalizedCache, "abstract_controller/localized_cache"
autoload :Logger, "abstract_controller/logger"
autoload :RenderingController, "abstract_controller/rendering_controller"
# === Exceptions
autoload :ActionNotFound, "abstract_controller/exceptions"
autoload :DoubleRenderError, "abstract_controller/exceptions"
autoload :Error, "abstract_controller/exceptions"
end
module ActionController
class Base < Metal
abstract!
include AbstractController::Callbacks
include AbstractController::Logger
include ActionController::Helpers
include ActionController::HideActions
include ActionController::UrlFor
include ActionController::Redirector
include ActionController::RenderingController
include ActionController::RenderOptions::All
include ActionController::Layouts
include ActionController::ConditionalGet
include ActionController::RackConvenience
include ActionController::Benchmarking
include ActionController::Configuration
# Legacy modules
include SessionManagement
include ActionDispatch::StatusCodes
include ActionController::Caching
include ActionController::MimeResponds
# Rails 2.x compatibility
include ActionController::Rails2Compatibility
include ActionController::Cookies
include ActionController::Session
include ActionController::Flash
include ActionController::Verification
include ActionController::RequestForgeryProtection
include ActionController::Streaming
include ActionController::HttpAuthentication::Basic::ControllerMethods
include ActionController::HttpAuthentication::Digest::ControllerMethods
include ActionController::FilterParameterLogging
include ActionController::Translation
# TODO: Extract into its own module
# This should be moved together with other normalizing behavior
module ImplicitRender
def send_action(*)
ret = super
default_render unless response_body
ret
end
def default_render
render
end
def method_for_action(action_name)
super || begin
if template_exists?(action_name.to_s, {:formats => formats}, :_prefix => controller_path)
"default_render"
end
end
end
end
include ImplicitRender
include ActionController::Rescue
def self.inherited(klass)
::ActionController::Base.subclasses << klass.to_s
super
end
def self.subclasses
@subclasses ||= []
end
def _normalize_options(action = nil, options = {}, &blk)
if action.is_a?(Hash)
options, action = action, nil
elsif action.is_a?(String) || action.is_a?(Symbol)
key = case action = action.to_s
when %r{^/} then :file
when %r{/} then :template
else :action
end
options.merge! key => action
elsif action
options.merge! :partial => action
end
if options.key?(:action) && options[:action].to_s.index("/")
options[:template] = options.delete(:action)
end
if options[:status]
options[:status] = interpret_status(options[:status]).to_i
end
options[:update] = blk if block_given?
options
end
def render(action = nil, options = {}, &blk)
options = _normalize_options(action, options, &blk)
super(options)
end
def render_to_string(action = nil, options = {}, &blk)
options = _normalize_options(action, options, &blk)
super(options)
end
# Redirects the browser to the target specified in +options+. This parameter can take one of three forms:
#
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
# * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
# * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) - Is passed straight through as the target for redirection.
# * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
# * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places.
# Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt>
#
# Examples:
# redirect_to :action => "show", :id => 5
# redirect_to post
# redirect_to "http://www.rubyonrails.org"
# redirect_to "/images/screenshot.jpg"
# redirect_to articles_url
# redirect_to :back
#
# The redirection happens as a "302 Moved" header unless otherwise specified.
#
# Examples:
# redirect_to post_url(@post), :status=>:found
# redirect_to :action=>'atom', :status=>:moved_permanently
# redirect_to post_url(@post), :status=>301
# redirect_to :action=>'atom', :status=>302
#
# When using <tt>redirect_to :back</tt>, if there is no referrer,
# RedirectBackError will be raised. You may specify some fallback
# behavior for this case by rescuing RedirectBackError.
def redirect_to(options = {}, response_status = {}) #:doc:
raise ActionControllerError.new("Cannot redirect to nil!") if options.nil?
status = if options.is_a?(Hash) && options.key?(:status)
interpret_status(options.delete(:status))
elsif response_status.key?(:status)
interpret_status(response_status[:status])
else
302
end
url = case options
# The scheme name consist of a letter followed by any combination of
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
# characters; and is terminated by a colon (":").
when %r{^\w[\w\d+.-]*:.*}
options
when String
request.protocol + request.host_with_port + options
when :back
raise RedirectBackError unless refer = request.headers["Referer"]
refer
else
url_for(options)
end
super(url, status)
end
end
end
require 'set'
module ActionController #:nodoc:
module Caching
# Action caching is similar to page caching by the fact that the entire
# output of the response is cached, but unlike page caching, every
# request still goes through the Action Pack. The key benefit
# of this is that filters are run before the cache is served, which
# allows for authentication and other restrictions on whether someone
# is allowed to see the cache. Example:
#
# class ListsController < ApplicationController
# before_filter :authenticate, :except => :public
# caches_page :public
# caches_action :index, :show, :feed
# end
#
# In this example, the public action doesn't require authentication,
# so it's possible to use the faster page caching method. But both
# the show and feed action are to be shielded behind the authenticate
# filter, so we need to implement those as action caches.
#
# Action caching internally uses the fragment caching and an around
# filter to do the job. The fragment cache is named according to both
# the current host and the path. So a page that is accessed at
# http://david.somewhere.com/lists/show/1 will result in a fragment named
# "david.somewhere.com/lists/show/1". This allows the cacher to
# differentiate between "david.somewhere.com/lists/" and
# "jamis.somewhere.com/lists/" -- which is a helpful way of assisting
# the subdomain-as-account-key pattern.
#
# Different representations of the same resource, e.g.
# <tt>http://david.somewhere.com/lists</tt> and
# <tt>http://david.somewhere.com/lists.xml</tt>
# are treated like separate requests and so are cached separately.
# Keep in mind when expiring an action cache that
# <tt>:action => 'lists'</tt> is not the same as
# <tt>:action => 'list', :format => :xml</tt>.
#
# You can set modify the default action cache path by passing a
# :cache_path option. This will be passed directly to
# ActionCachePath.path_for. This is handy for actions with multiple
# possible routes that should be cached differently. If a block is
# given, it is called with the current controller instance.
#
# And you can also use :if (or :unless) to pass a Proc that
# specifies when the action should be cached.
#
# Finally, if you are using memcached, you can also pass :expires_in.
#
# class ListsController < ApplicationController
# before_filter :authenticate, :except => :public
# caches_page :public
# caches_action :index, :if => proc do |c|
# !c.request.format.json? # cache if is not a JSON request
# end
#
# caches_action :show, :cache_path => { :project => 1 },
# :expires_in => 1.hour
#
# caches_action :feed, :cache_path => proc do |controller|
# if controller.params[:user_id]
# controller.send(:user_list_url,
# controller.params[:user_id], controller.params[:id])
# else
# controller.send(:list_url, controller.params[:id])
# end
# end
# end
#
# If you pass :layout => false, it will only cache your action
# content. It is useful when your layout has dynamic information.
#
module Actions
extend ActiveSupport::Concern
module ClassMethods
# Declares that +actions+ should be cached.
# See ActionController::Caching::Actions for details.
def caches_action(*actions)
return unless cache_configured?
options = actions.extract_options!
filter_options = options.extract!(:if, :unless).merge(:only => actions)
cache_options = options.extract!(:layout, :cache_path).merge(:store_options => options)
around_filter ActionCacheFilter.new(cache_options), filter_options
end
end
def _render_cache_fragment(cache, extension, layout)
render :text => cache, :layout => layout, :content_type => Mime[extension || :html]
end
def _save_fragment(name, layout, options)
return unless caching_allowed?
content = layout ? view_context.content_for(:layout) : response_body
write_fragment(name, content, options)
end
protected
def expire_action(options = {})
return unless cache_configured?
actions = options[:action]
if actions.is_a?(Array)
actions.each {|action| expire_action(options.merge(:action => action)) }
else
expire_fragment(ActionCachePath.new(self, options, false).path)
end
end
class ActionCacheFilter #:nodoc:
def initialize(options, &block)
@cache_path, @store_options, @layout =
options.values_at(:cache_path, :store_options, :layout)
end
def filter(controller)
path_options = if @cache_path.respond_to?(:call)
controller.instance_exec(controller, &@cache_path)
else
@cache_path
end
cache_path = ActionCachePath.new(controller, path_options || {})
if cache = controller.read_fragment(cache_path.path, @store_options)
controller._render_cache_fragment(cache, cache_path.extension, @layout == false)
else
yield
controller._save_fragment(cache_path.path, @layout == false, @store_options)
end
end
end
class ActionCachePath
attr_reader :path, :extension
# If +infer_extension+ is true, the cache path extension is looked up from the request's
# path & format. This is desirable when reading and writing the cache, but not when
# expiring the cache - expire_action should expire the same files regardless of the
# request format.
def initialize(controller, options = {}, infer_extension = true)
if infer_extension
@extension = controller.params[:format]
options.reverse_merge!(:format => @extension) if options.is_a?(Hash)
end
path = controller.url_for(options).split(%r{://}).last
@path = normalize!(path)
end
private
def normalize!(path)
path << 'index' if path[-1] == ?/
path << ".#{extension}" if extension and !path.ends_with?(extension)
URI.unescape(path)
end
end
end
end
end
module ActionController #:nodoc:
module Caching
# Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
# certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
# parties. The caching is done using the cache helper available in the Action View. A template with caching might look something like:
#
# <b>Hello <%= @name %></b>
# <% cache do %>
# All the topics in the system:
# <%= render :partial => "topic", :collection => Topic.find(:all) %>
# <% end %>
#
# This cache will bind to the name of the action that called it, so if this code was part of the view for the topics/list action, you would
# be able to invalidate it using <tt>expire_fragment(:controller => "topics", :action => "list")</tt>.
#
# This default behavior is of limited use if you need to cache multiple fragments per action or if the action itself is cached using
# <tt>caches_action</tt>, so we also have the option to qualify the name of the cached fragment with something like:
#
# <% cache(:action => "list", :action_suffix => "all_topics") do %>
#
# That would result in a name such as "/topics/list/all_topics", avoiding conflicts with the action cache and with any fragments that use a
# different suffix. Note that the URL doesn't have to really exist or be callable - the url_for system is just used to generate unique
# cache names that we can refer to when we need to expire the cache.
#
# The expiration call for this example is:
#
# expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")
module Fragments
# Given a key (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading,
# writing, or expiring a cached fragment. If the key is a hash, the generated key is the return
# value of url_for on that hash (without the protocol). All keys are prefixed with "views/" and uses
# ActiveSupport::Cache.expand_cache_key for the expansion.
def fragment_cache_key(key)
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
end
def fragment_for(buffer, name = {}, options = nil, &block) #:nodoc:
if perform_caching
if fragment_exist?(name,options)
buffer.concat(read_fragment(name, options))
else
pos = buffer.length
block.call
write_fragment(name, buffer[pos..-1], options)
end
else
block.call
end
end
# Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)
def write_fragment(key, content, options = nil)
return content unless cache_configured?
key = fragment_cache_key(key)
ActiveSupport::Notifications.instrument(:write_fragment, :key => key) do
cache_store.write(key, content, options)
end
content
end
# Reads a cached fragment from the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)
def read_fragment(key, options = nil)
return unless cache_configured?
key = fragment_cache_key(key)
ActiveSupport::Notifications.instrument(:read_fragment, :key => key) do
cache_store.read(key, options)
end
end
# Check if a cached fragment from the location signified by <tt>key</tt> exists (see <tt>expire_fragment</tt> for acceptable formats)
def fragment_exist?(key, options = nil)
return unless cache_configured?
key = fragment_cache_key(key)
ActiveSupport::Notifications.instrument(:fragment_exist?, :key => key) do
cache_store.exist?(key, options)
end
end
# Removes fragments from the cache.
#
# +key+ can take one of three forms:
# * String - This would normally take the form of a path, like
# <tt>"pages/45/notes"</tt>.
# * Hash - Treated as an implicit call to +url_for+, like
# <tt>{:controller => "pages", :action => "notes", :id => 45}</tt>
# * Regexp - Will remove any fragment that matches, so
# <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
# don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
# the actual filename matched looks like
# <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
# only supported on caches that can iterate over all keys (unlike
# memcached).
#
# +options+ is passed through to the cache store's <tt>delete</tt>
# method (or <tt>delete_matched</tt>, for Regexp keys.)
def expire_fragment(key, options = nil)
return unless cache_configured?
key = fragment_cache_key(key) unless key.is_a?(Regexp)
message = nil
ActiveSupport::Notifications.instrument(:expire_fragment, :key => key) do
if key.is_a?(Regexp)
message = "Expired fragments matching: #{key.source}"
cache_store.delete_matched(key, options)
else
message = "Expired fragment: #{key}"
cache_store.delete(key, options)
end
end
end
end
end
end
require 'fileutils'
require 'uri'
module ActionController #:nodoc:
module Caching
# Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server
# can serve without going through Action Pack. This is the fastest way to cache your content as opposed to going dynamically
# through the process of generating the content. Unfortunately, this incredible speed-up is only available to stateless pages
# where all visitors are treated the same. Content management systems -- including weblogs and wikis -- have many pages that are
# a great fit for this approach, but account-based systems where people log in and manipulate their own data are often less
# likely candidates.
#
# Specifying which actions to cache is done through the <tt>caches_page</tt> class method:
#
# class WeblogController < ActionController::Base
# caches_page :show, :new
# end
#
# This will generate cache files such as <tt>weblog/show/5.html</tt> and <tt>weblog/new.html</tt>,
# which match the URLs used to trigger the dynamic generation. This is how the web server is able
# pick up a cache file when it exists and otherwise let the request pass on to Action Pack to generate it.
#
# Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache
# is not restored before another hit is made against it. The API for doing so mimics the options from +url_for+ and friends:
#
# class WeblogController < ActionController::Base
# def update
# List.update(params[:list][:id], params[:list])
# expire_page :action => "show", :id => params[:list][:id]
# redirect_to :action => "show", :id => params[:list][:id]
# end
# end
#
# Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be
# expired.
module Pages
def self.included(base) #:nodoc:
base.extend(ClassMethods)
base.class_eval do
@@page_cache_directory = defined?(Rails.public_path) ? Rails.public_path : ""
##
# :singleton-method:
# The cache directory should be the document root for the web server and is set using <tt>Base.page_cache_directory = "/document/root"</tt>.
# For Rails, this directory has already been set to Rails.public_path (which is usually set to <tt>RAILS_ROOT + "/public"</tt>). Changing
# this setting can be useful to avoid naming conflicts with files in <tt>public/</tt>, but doing so will likely require configuring your
# web server to look in the new location for cached files.
cattr_accessor :page_cache_directory
@@page_cache_extension = '.html'
##
# :singleton-method:
# Most Rails requests do not have an extension, such as <tt>/weblog/new</tt>. In these cases, the page caching mechanism will add one in
# order to make it easy for the cached files to be picked up properly by the web server. By default, this cache extension is <tt>.html</tt>.
# If you want something else, like <tt>.php</tt> or <tt>.shtml</tt>, just set Base.page_cache_extension. In cases where a request already has an
# extension, such as <tt>.xml</tt> or <tt>.rss</tt>, page caching will not add an extension. This allows it to work well with RESTful apps.
cattr_accessor :page_cache_extension
end
end
module ClassMethods
# Expires the page that was cached with the +path+ as a key. Example:
# expire_page "/lists/show"
def expire_page(path)
return unless perform_caching
path = page_cache_path(path)
ActiveSupport::Notifications.instrument(:expire_page, :path => path) do
File.delete(path) if File.exist?(path)
end
end
# Manually cache the +content+ in the key determined by +path+. Example:
# cache_page "I'm the cached content", "/lists/show"
def cache_page(content, path)
return unless perform_caching
path = page_cache_path(path)
ActiveSupport::Notifications.instrument(:cache_page, :path => path) do
FileUtils.makedirs(File.dirname(path))
File.open(path, "wb+") { |f| f.write(content) }
end
end
# Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that
# matches the triggering url.
#
# Usage:
#
# # cache the index action
# caches_page :index
#
# # cache the index action except for JSON requests
# caches_page :index, :if => Proc.new { |c| !c.request.format.json? }
def caches_page(*actions)
return unless perform_caching
options = actions.extract_options!
after_filter({:only => actions}.merge(options)) { |c| c.cache_page }
end
private
def page_cache_file(path)
name = (path.empty? || path == "/") ? "/index" : URI.unescape(path.chomp('/'))
name << page_cache_extension unless (name.split('/').last || name).include? '.'
return name
end
def page_cache_path(path)
page_cache_directory + page_cache_file(path)
end
end
# Expires the page that was cached with the +options+ as a key. Example:
# expire_page :controller => "lists", :action => "show"
def expire_page(options = {})
return unless perform_caching
if options.is_a?(Hash)
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action)))
end
else
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true)))
end
else
self.class.expire_page(options)
end
end
# Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of response.body is used
# If no options are provided, the requested url is used. Example:
# cache_page "I'm the cached content", :controller => "lists", :action => "show"
def cache_page(content = nil, options = nil)
return unless perform_caching && caching_allowed
path = case options
when Hash
url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format]))
when String
options
else
request.path
end
self.class.cache_page(content || response.body, path)
end
private
def caching_allowed
request.get? && response.status.to_i == 200
end
end
end
end
module ActionController #:nodoc:
module Caching
# Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
# They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
#
# class ListSweeper < ActionController::Caching::Sweeper
# observe List, Item
#
# def after_save(record)
# list = record.is_a?(List) ? record : record.list
# expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
# expire_action(:controller => "lists", :action => "all")
# list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
# end
# end
#
# The sweeper is assigned in the controllers that wish to have its job performed using the <tt>cache_sweeper</tt> class method:
#
# class ListsController < ApplicationController
# caches_action :index, :show, :public, :feed
# cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
# end
#
# In the example above, four actions are cached and three actions are responsible for expiring those caches.
#
# You can also name an explicit class in the declaration of a sweeper, which is needed if the sweeper is in a module:
#
# class ListsController < ApplicationController
# caches_action :index, :show, :public, :feed
# cache_sweeper OpenBar::Sweeper, :only => [ :edit, :destroy, :share ]
# end
module Sweeping
def self.included(base) #:nodoc:
base.extend(ClassMethods)
end
module ClassMethods #:nodoc:
def cache_sweeper(*sweepers)
configuration = sweepers.extract_options!
sweepers.each do |sweeper|
ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
sweeper_instance = (sweeper.is_a?(Symbol) ? Object.const_get(sweeper.to_s.classify) : sweeper).instance
if sweeper_instance.is_a?(Sweeper)
around_filter(sweeper_instance, :only => configuration[:only])
else
after_filter(sweeper_instance, :only => configuration[:only])
end
end
end
end
end
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
protected
# gets the action cache path for the given options.
def action_path_for(options)
Actions::ActionCachePath.new(controller, options).path
end
# Retrieve instance variables set in the controller.
def assigns(key)
controller.instance_variable_get("@#{key}")
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments, &block)
return if @controller.nil?
@controller.__send__(method, *arguments, &block)
end
end
end
end
end
require 'fileutils'
require 'uri'
require 'set'
module ActionController #:nodoc:
# Caching is a cheap way of speeding up slow applications by keeping the result of
# calculations, renderings, and database calls around for subsequent requests.
# Action Controller affords you three approaches in varying levels of granularity:
# Page, Action, Fragment.
#
# You can read more about each approach and the sweeping assistance by clicking the
# modules below.
#
# Note: To turn off all caching and sweeping, set
# config.action_controller.perform_caching = false.
#
# == Caching stores
#
# All the caching stores from ActiveSupport::Cache are available to be used as backends
# for Action Controller caching. This setting only affects action and fragment caching
# as page caching is always written to disk.
#
# Configuration examples (MemoryStore is the default):
#
# config.action_controller.cache_store = :memory_store
# config.action_controller.cache_store = :file_store, "/path/to/cache/directory"
# config.action_controller.cache_store = :drb_store, "druby://localhost:9192"
# config.action_controller.cache_store = :mem_cache_store, "localhost"
# config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new("localhost:11211")
# config.action_controller.cache_store = MyOwnStore.new("parameter")
module Caching
extend ActiveSupport::Concern
autoload :Actions, 'action_controller/caching/actions'
autoload :Fragments, 'action_controller/caching/fragments'
autoload :Pages, 'action_controller/caching/pages'
autoload :Sweeper, 'action_controller/caching/sweeping'
autoload :Sweeping, 'action_controller/caching/sweeping'
included do
@@cache_store = nil
cattr_reader :cache_store
# Defines the storage option for cached fragments
def self.cache_store=(store_option)
@@cache_store = ActiveSupport::Cache.lookup_store(store_option)
end
include Pages, Actions, Fragments
include Sweeping if defined?(ActiveRecord)
@@perform_caching = true
cattr_accessor :perform_caching
end
module ClassMethods
def cache_configured?
perform_caching && cache_store
end
end
def caching_allowed?
request.get? && response.status == 200
end
protected
# Convenience accessor
def cache(key, options = {}, &block)
if cache_configured?
cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
else
yield
end
end
private
def cache_configured?
self.class.cache_configured?
end
end
end
ActionController::Integration = ActionDispatch::Integration
ActionController::IntegrationTest = ActionDispatch::IntegrationTest
ActionController::PerformanceTest = ActionDispatch::PerformanceTest
ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request
ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response
ActionController::Routing = ActionDispatch::Routing
ActionController::Routing::Routes = ActionDispatch::Routing::RouteSet.new
require 'active_support/core_ext/module/delegation'
module ActionController
# Dispatches requests to the appropriate controller and takes care of
# reloading the app after each request when Dependencies.load? is true.
class Dispatcher
cattr_accessor :prepare_each_request
self.prepare_each_request = false
class << self
def define_dispatcher_callbacks(cache_classes)
unless cache_classes
# Run prepare callbacks before every request in development mode
self.prepare_each_request = true
# Development mode callbacks
ActionDispatch::Callbacks.before_dispatch do |app|
ActionController::Routing::Routes.reload
end
ActionDispatch::Callbacks.after_dispatch do
# Cleanup the application before processing the current request.
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
ActiveSupport::Dependencies.clear
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
end
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
end
if defined?(ActiveRecord)
to_prepare(:activerecord_instantiate_observers) do
ActiveRecord::Base.instantiate_observers
end
end
if Base.logger && Base.logger.respond_to?(:flush)
after_dispatch do
Base.logger.flush
end
end
to_prepare do
I18n.reload!
end
end
delegate :to_prepare, :before_dispatch, :around_dispatch, :after_dispatch,
:to => ActionDispatch::Callbacks
def new
# DEPRECATE Rails application fallback
Rails.application
end
end
end
end
require 'active_support/core_ext/benchmark'
module ActionController #:nodoc:
# The benchmarking module times the performance of actions and reports to the logger. If the Active Record
# package has been included, a separate timing section for database calls will be added as well.
module Benchmarking #:nodoc:
extend ActiveSupport::Concern
protected
def render(*args, &block)
if logger
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
db_runtime = ActiveRecord::Base.connection.reset_runtime
end
render_output = nil
@view_runtime = Benchmark.ms { render_output = super }
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
@db_rt_before_render = db_runtime
@db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
@view_runtime -= @db_rt_after_render
end
render_output
else
super
end
end
private
def process_action(*args)
if logger
ms = [Benchmark.ms { super }, 0.01].max
logging_view = defined?(@view_runtime)
logging_active_record = Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message = 'Completed in %.0fms' % ms
if logging_view || logging_active_record
log_message << " ("
log_message << view_runtime if logging_view
if logging_active_record
log_message << ", " if logging_view
log_message << active_record_runtime + ")"
else
")"
end
end
log_message << " | #{response.status}"
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
response.headers["X-Runtime"] = "%.0f" % ms
else
super
end
end
def view_runtime
"View: %.0f" % @view_runtime
end
def active_record_runtime
db_runtime = ActiveRecord::Base.connection.reset_runtime
db_runtime += @db_rt_before_render if @db_rt_before_render
db_runtime += @db_rt_after_render if @db_rt_after_render
"DB: %.0f" % db_runtime
end
end
end
module ActionController
module Rails2Compatibility
extend ActiveSupport::Concern
class ::ActionController::ActionControllerError < StandardError #:nodoc:
end
# Temporary hax
included do
::ActionController::UnknownAction = ::AbstractController::ActionNotFound
::ActionController::DoubleRenderError = ::AbstractController::DoubleRenderError
cattr_accessor :session_options
self.session_options = {}
cattr_accessor :allow_concurrency
self.allow_concurrency = false
cattr_accessor :relative_url_root
self.relative_url_root = ENV['RAILS_RELATIVE_URL_ROOT']
class << self
delegate :default_charset=, :to => "ActionDispatch::Response"
end
# cattr_reader :protected_instance_variables
cattr_accessor :protected_instance_variables
self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render
@variables_added @request_origin @url
@parent_controller @action_name
@before_filter_chain_aborted @_headers @_params
@_flash @_response)
# Indicates whether or not optimise the generated named
# route helper methods
cattr_accessor :optimise_named_routes
self.optimise_named_routes = true
cattr_accessor :resources_path_names
self.resources_path_names = { :new => 'new', :edit => 'edit' }
# Controls the resource action separator
cattr_accessor :resource_action_separator
self.resource_action_separator = "/"
cattr_accessor :use_accept_header
self.use_accept_header = true
cattr_accessor :page_cache_directory
self.page_cache_directory = defined?(Rails.public_path) ? Rails.public_path : ""
cattr_reader :cache_store
cattr_accessor :consider_all_requests_local
self.consider_all_requests_local = true
# Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets,
# and images to a dedicated asset server away from the main web server. Example:
# ActionController::Base.asset_host = "http://assets.example.com"
cattr_accessor :asset_host
cattr_accessor :ip_spoofing_check
self.ip_spoofing_check = true
cattr_accessor :trusted_proxies
end
# For old tests
def initialize_template_class(*) end
def assign_shortcuts(*) end
# TODO: Remove this after we flip
def template
@template ||= view_context
end
def process_action(*)
template
super
end
module ClassMethods
def consider_all_requests_local
end
def rescue_action(env)
raise env["action_dispatch.rescue.exception"]
end
# Defines the storage option for cached fragments
def cache_store=(store_option)
@@cache_store = ActiveSupport::Cache.lookup_store(store_option)
end
end
def render_to_body(options)
if options.is_a?(Hash) && options.key?(:template)
options[:template].sub!(/^\//, '')
end
options[:text] = nil if options.delete(:nothing) == true
options[:text] = " " if options.key?(:text) && options[:text].nil?
super || " "
end
def _handle_method_missing
method_missing(@_action_name.to_sym)
end
def method_for_action(action_name)
super || (respond_to?(:method_missing) && "_handle_method_missing")
end
def _find_layout(name, details)
details[:prefix] = nil if name =~ /\blayouts/
super
end
# Move this into a "don't run in production" module
def _default_layout(details, require_layout = false)
super
rescue ActionView::MissingTemplate
_find_layout(_layout({}), {})
nil
end
def performed?
response_body
end
# ==== Request only view path switching ====
def append_view_path(path)
view_paths.push(*path)
end
def prepend_view_path(path)
view_paths.unshift(*path)
end
def view_paths
view_context.view_paths
end
end
end
module ActionController
module ConditionalGet
extend ActiveSupport::Concern
include RackConvenience
include Head
# Sets the etag, last_modified, or both on the response and renders a
# "304 Not Modified" response if the request is already fresh.
#
# Parameters:
# * <tt>:etag</tt>
# * <tt>:last_modified</tt>
# * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
#
# Example:
#
# def show
# @article = Article.find(params[:id])
# fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true)
# end
#
# This will render the show template if the request isn't sending a matching etag or
# If-Modified-Since header and just a "304 Not Modified" response if there's a match.
#
def fresh_when(options)
options.assert_valid_keys(:etag, :last_modified, :public)
response.etag = options[:etag] if options[:etag]
response.last_modified = options[:last_modified] if options[:last_modified]
response.cache_control[:public] = true if options[:public]
head :not_modified if request.fresh?(response)
end
# Sets the etag and/or last_modified on the response and checks it against
# the client request. If the request doesn't match the options provided, the
# request is considered stale and should be generated from scratch. Otherwise,
# it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent.
#
# Parameters:
# * <tt>:etag</tt>
# * <tt>:last_modified</tt>
# * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
#
# Example:
#
# def show
# @article = Article.find(params[:id])
#
# if stale?(:etag => @article, :last_modified => @article.created_at.utc)
# @statistics = @article.really_expensive_call
# respond_to do |format|
# # all the supported formats
# end
# end
# end
def stale?(options)
fresh_when(options)
!request.fresh?(response)
end
# Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that
# intermediate caches shouldn't cache the response.
#
# Examples:
# expires_in 20.minutes
# expires_in 3.hours, :public => true
# expires in 3.hours, 'max-stale' => 5.hours, :public => true
#
# This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
def expires_in(seconds, options = {}) #:doc:
response.cache_control.merge!(:max_age => seconds, :public => options.delete(:public))
options.delete(:private)
response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"}
end
# Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or
# intermediate caches (like caching proxy servers).
def expires_now #:doc:
response.cache_control.replace(:no_cache => true)
end
end
end
module ActionController
module Configuration
extend ActiveSupport::Concern
def config
@config ||= self.class.config
end
def config=(config)
@config = config
end
module ClassMethods
def default_config
@default_config ||= {}
end
def config
self.config ||= default_config
end
def config=(config)
@config = ActiveSupport::OrderedHash.new
@config.merge!(config)
end
end
end
endmodule ActionController #:nodoc:
# Cookies are read and written through ActionController#cookies.
#
# The cookies being read are the ones received along with the request, the cookies
# being written will be sent out with the response. Reading a cookie does not get
# the cookie object itself back, just the value it holds.
#
# Examples for writing:
#
# # Sets a simple session cookie.
# cookies[:user_name] = "david"
#
# # Sets a cookie that expires in 1 hour.
# cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
#
# Examples for reading:
#
# cookies[:user_name] # => "david"
# cookies.size # => 2
#
# Example for deleting:
#
# cookies.delete :user_name
#
# Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
#
# cookies[:key] = {
# :value => 'a yummy cookie',
# :expires => 1.year.from_now,
# :domain => 'domain.com'
# }
#
# cookies.delete(:key, :domain => 'domain.com')
#
# The option symbols for setting cookies are:
#
# * <tt>:value</tt> - The cookie's value or list of values (as an array).
# * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
# of the application.
# * <tt>:domain</tt> - The domain for which this cookie applies.
# * <tt>:expires</tt> - The time at which this cookie expires, as a Time object.
# * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
# Default is +false+.
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
# only HTTP. Defaults to +false+.
module Cookies
extend ActiveSupport::Concern
include RackConvenience
included do
helper_method :cookies
end
protected
# Returns the cookie container, which operates as described above.
def cookies
@cookies ||= CookieJar.build(request, response)
end
end
class CookieJar < Hash #:nodoc:
def self.build(request, response)
new.tap do |hash|
hash.update(request.cookies)
hash.response = response
end
end
attr_accessor :response
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
def [](name)
super(name.to_s)
end
# Sets the cookie named +name+. The second argument may be the very cookie
# value, or a hash of options as documented above.
def []=(key, options)
if options.is_a?(Hash)
options.symbolize_keys!
value = options[:value]
else
value = options
options = { :value => value }
end
super(key.to_s, value)
options[:path] ||= "/"
response.set_cookie(key, options)
end
# Removes the cookie on the client machine by setting the value to an empty string
# and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
def delete(key, options = {})
options.symbolize_keys!
options[:path] ||= "/"
value = super(key.to_s)
response.delete_cookie(key, options)
value
end
end
end
module ActionController
class ActionControllerError < StandardError #:nodoc:
end
class RenderError < ActionControllerError #:nodoc:
end
class RoutingError < ActionControllerError #:nodoc:
attr_reader :failures
def initialize(message, failures=[])
super(message)
@failures = failures
end
end
class MethodNotAllowed < ActionControllerError #:nodoc:
attr_reader :allowed_methods
def initialize(*allowed_methods)
super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.")
@allowed_methods = allowed_methods
end
def allowed_methods_header
allowed_methods.map { |method_symbol| method_symbol.to_s.upcase } * ', '
end
def handle_response!(response)
response.headers['Allow'] ||= allowed_methods_header
end
end
class NotImplemented < MethodNotAllowed #:nodoc:
end
class UnknownController < ActionControllerError #:nodoc:
end
class MissingFile < ActionControllerError #:nodoc:
end
class RenderError < ActionControllerError #:nodoc:
end
class SessionOverflowError < ActionControllerError #:nodoc:
DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.'
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
class UnknownHttpMethod < ActionControllerError #:nodoc:
end
endmodule ActionController
module FilterParameterLogging
extend ActiveSupport::Concern
include AbstractController::Logger
module ClassMethods
# Replace sensitive parameter data from the request log.
# Filters parameters that have any of the arguments as a substring.
# Looks in all subhashes of the param hash for keys to filter.
# If a block is given, each key and value of the parameter hash and all
# subhashes is passed to it, the value or key
# can be replaced using String#replace or similar method.
#
# Examples:
#
# filter_parameter_logging :password
# => replaces the value to all keys matching /password/i with "[FILTERED]"
#
# filter_parameter_logging :foo, "bar"
# => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
#
# filter_parameter_logging { |k,v| v.reverse! if k =~ /secret/i }
# => reverses the value to all keys matching /secret/i
#
# filter_parameter_logging(:foo, "bar") { |k,v| v.reverse! if k =~ /secret/i }
# => reverses the value to all keys matching /secret/i, and
# replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
def filter_parameter_logging(*filter_words, &block)
raise "You must filter at least one word from logging" if filter_words.empty?
parameter_filter = Regexp.new(filter_words.join('|'), true)
define_method(:filter_parameters) do |original_params|
filtered_params = {}
original_params.each do |key, value|
if key =~ parameter_filter
value = '[FILTERED]'
elsif value.is_a?(Hash)
value = filter_parameters(value)
elsif value.is_a?(Array)
value = value.map { |item| filter_parameters(item) }
elsif block_given?
key = key.dup
value = value.dup if value.duplicable?
yield key, value
end
filtered_params[key] = value
end
filtered_params
end
protected :filter_parameters
end
end
INTERNAL_PARAMS = [:controller, :action, :format, :_method, :only_path]
def process(*)
response = super
if logger
parameters = filter_parameters(params).except!(*INTERNAL_PARAMS)
logger.info { " Parameters: #{parameters.inspect}" } unless parameters.empty?
end
response
end
protected
def filter_parameters(params)
params.dup
end
end
end
module ActionController #:nodoc:
# The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done. Example:
#
# class PostsController < ActionController::Base
# def create
# # save post
# flash[:notice] = "Successfully created post"
# redirect_to posts_path(@post)
# end
#
# def show
# # doesn't need to assign the flash notice to the template, that's done automatically
# end
# end
#
# show.html.erb
# <% if flash[:notice] %>
# <div class="notice"><%= flash[:notice] %></div>
# <% end %>
#
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as
# many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
#
# See docs on the FlashHash class for more details about the flash.
module Flash
extend ActiveSupport::Concern
include Session
class FlashNow #:nodoc:
def initialize(flash)
@flash = flash
end
def []=(k, v)
@flash[k] = v
@flash.discard(k)
v
end
def [](k)
@flash[k]
end
end
class FlashHash < Hash
def initialize #:nodoc:
super
@used = Set.new
end
def []=(k, v) #:nodoc:
keep(k)
super
end
def update(h) #:nodoc:
h.keys.each { |k| keep(k) }
super
end
alias :merge! :update
def replace(h) #:nodoc:
@used = Set.new
super
end
# Sets a flash that will not be available to the next action, only to the current.
#
# flash.now[:message] = "Hello current action"
#
# This method enables you to use the flash as a central messaging system in your app.
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
# vanish when the current action is done.
#
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
def now
FlashNow.new(self)
end
# Keeps either the entire current flash or a specific flash entry available for the next action:
#
# flash.keep # keeps the entire flash
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
def keep(k = nil)
use(k, false)
end
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
#
# flash.discard # discard the entire flash at the end of the current action
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
def discard(k = nil)
use(k)
end
# Mark for removal entries that were kept, and delete unkept ones.
#
# This method is called automatically by filters, so you generally don't need to care about it.
def sweep #:nodoc:
keys.each do |k|
unless @used.include?(k)
@used << k
else
delete(k)
@used.delete(k)
end
end
# clean up after keys that could have been left over by calling reject! or shift on the flash
(@used - keys).each{ |k| @used.delete(k) }
end
def store(session)
return if self.empty?
session["flash"] = self
end
private
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used
# use('msg') # marks the "msg" entry as used
# use(nil, false) # marks the entire flash as unused (keeps it around for one more action)
# use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action)
# Returns the single value for the key you asked to be marked (un)used or the FlashHash itself
# if no key is passed.
def use(key = nil, used = true)
Array(key || keys).each { |k| used ? @used << k : @used.delete(k) }
return key ? self[key] : self
end
end
protected
def process_action(method_name)
super
@_flash.store(session) if @_flash
@_flash = nil
end
def reset_session
super
@_flash = nil
end
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
def flash #:doc:
unless @_flash
@_flash = session["flash"] || FlashHash.new
@_flash.sweep
end
@_flash
end
end
end
module ActionController
module Head
# Return a response that has no content (merely headers). The options
# argument is interpreted to be a hash of header names and values.
# This allows you to easily return a response that consists only of
# significant headers:
#
# head :created, :location => person_path(@person)
#
# It can also be used to return exceptional conditions:
#
# return head(:method_not_allowed) unless request.post?
# return head(:bad_request) unless valid_request?
# render
def head(status, options = {})
options, status = status, nil if status.is_a?(Hash)
status ||= options.delete(:status) || :ok
location = options.delete(:location)
options.each do |key, value|
headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s
end
render :nothing => true, :status => status, :location => location
end
end
endmodule ActionController
# The Rails framework provides a large number of helpers for working with +assets+, +dates+, +forms+,
# +numbers+ and model objects, to name a few. These helpers are available to all templates
# by default.
#
# In addition to using the standard template helpers provided in the Rails framework, creating custom helpers to
# extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will
# include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically
# include <tt>MyHelper</tt>.
#
# Additional helpers can be specified using the +helper+ class method in <tt>ActionController::Base</tt> or any
# controller which inherits from it.
#
# ==== Examples
# The +to_s+ method from the Time class can be wrapped in a helper method to display a custom message if
# the Time object is blank:
#
# module FormattedTimeHelper
# def format_time(time, format=:long, blank_message="&nbsp;")
# time.blank? ? blank_message : time.to_s(format)
# end
# end
#
# FormattedTimeHelper can now be included in a controller, using the +helper+ class method:
#
# class EventsController < ActionController::Base
# helper FormattedTimeHelper
# def index
# @events = Event.find(:all)
# end
# end
#
# Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
#
# <% @events.each do |event| -%>
# <p>
# <% format_time(event.time, :short, "N/A") %> | <%= event.name %>
# </p>
# <% end -%>
#
# Finally, assuming we have two event instances, one which has a time and one which does not,
# the output might look like this:
#
# 23 Aug 11:30 | Carolina Railhawks Soccer Match
# N/A | Carolina Railhaws Training Workshop
#
module Helpers
extend ActiveSupport::Concern
include AbstractController::Helpers
included do
# Set the default directory for helpers
extlib_inheritable_accessor(:helpers_dir) do
defined?(Rails) ? "#{Rails.root}/app/helpers" : "app/helpers"
end
end
module ClassMethods
def inherited(klass)
klass.class_eval { default_helper_module! unless name.blank? }
super
end
# Declares helper accessors for controller attributes. For example, the
# following adds new +name+ and <tt>name=</tt> instance methods to a
# controller and makes them available to the view:
# helper_attr :name
# attr_accessor :name
#
# ==== Parameters
# *attrs<Array[String, Symbol]>:: Names of attributes to be converted
# into helpers.
def helper_attr(*attrs)
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
# Provides a proxy to access helpers methods from outside the view.
def helpers
@helper_proxy ||= ActionView::Base.new.extend(_helpers)
end
private
# Overwrite _modules_for_helpers to accept :all as argument, which loads
# all helpers in helpers_dir.
#
# ==== Parameters
# args<Array[String, Symbol, Module, all]>:: A list of helpers
#
# ==== Returns
# Array[Module]:: A normalized list of modules for the list of
# helpers provided.
def _modules_for_helpers(args)
args += all_application_helpers if args.delete(:all)
super(args)
end
def default_helper_module!
module_name = name.sub(/Controller$/, '')
module_path = module_name.underscore
helper module_path
rescue MissingSourceFile => e
raise e unless e.is_missing? "#{module_path}_helper"
rescue NameError => e
raise e unless e.missing_name? "#{module_name}Helper"
end
# Extract helper names from files in app/helpers/**/*.rb
def all_application_helpers
extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/
Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' }
end
end
end
end
module ActionController
# ActionController::HideActions adds the ability to prevent public methods on a controller
# to be called as actions.
module HideActions
extend ActiveSupport::Concern
included do
extlib_inheritable_accessor(:hidden_actions) { Set.new }
end
private
# Overrides AbstractController::Base#action_method? to return false if the
# action name is in the list of hidden actions.
def action_method?(action_name)
self.class.visible_action?(action_name) do
!hidden_actions.include?(action_name) && super
end
end
module ClassMethods
# Sets all of the actions passed in as hidden actions.
#
# ==== Parameters
# *args<#to_s>:: A list of actions
def hide_action(*args)
hidden_actions.merge(args.map! {|a| a.to_s })
end
def inherited(klass)
klass.instance_variable_set("@visible_actions", {})
super
end
def visible_action?(action_name)
return @visible_actions[action_name] if @visible_actions.key?(action_name)
@visible_actions[action_name] = yield
end
# Overrides AbstractController::Base#action_methods to remove any methods
# that are listed as hidden methods.
def action_methods
@action_methods ||= Set.new(super.reject {|name| hidden_actions.include?(name)})
end
end
end
end
require 'active_support/base64'
module ActionController
module HttpAuthentication
# Makes it dead easy to do HTTP Basic authentication.
#
# Simple Basic example:
#
# class PostsController < ApplicationController
# USER_NAME, PASSWORD = "dhh", "secret"
#
# before_filter :authenticate, :except => [ :index ]
#
# def index
# render :text => "Everyone can see me!"
# end
#
# def edit
# render :text => "I'm only accessible if you know the password"
# end
#
# private
# def authenticate
# authenticate_or_request_with_http_basic do |user_name, password|
# user_name == USER_NAME && password == PASSWORD
# end
# end
# end
#
#
# Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
# the regular HTML interface is protected by a session approach:
#
# class ApplicationController < ActionController::Base
# before_filter :set_account, :authenticate
#
# protected
# def set_account
# @account = Account.find_by_url_name(request.subdomains.first)
# end
#
# def authenticate
# case request.format
# when Mime::XML, Mime::ATOM
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
# @current_user = user
# else
# request_http_basic_authentication
# end
# else
# if session_authenticated?
# @current_user = @account.users.find(session[:authenticated][:user_id])
# else
# redirect_to(login_url) and return false
# end
# end
# end
# end
#
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
# get(
# "/notes/1.xml", nil,
# :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
# )
#
# assert_equal 200, status
# end
#
# Simple Digest example:
#
# require 'digest/md5'
# class PostsController < ApplicationController
# REALM = "SuperSecret"
# USERS = {"dhh" => "secret", #plain text password
# "dap" => Digest:MD5::hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password
#
# before_filter :authenticate, :except => [:index]
#
# def index
# render :text => "Everyone can see me!"
# end
#
# def edit
# render :text => "I'm only accessible if you know the password"
# end
#
# private
# def authenticate
# authenticate_or_request_with_http_digest(REALM) do |username|
# USERS[username]
# end
# end
# end
#
# NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password or the ha1 digest hash so the framework can appropriately
# hash to check the user's credentials. Returning +nil+ will cause authentication to fail.
# Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
# the password file or database is compromised, the attacker would be able to use the ha1 hash to
# authenticate as the user at this +realm+, but would not have the user's password to try using at
# other sites.
#
# On shared hosts, Apache sometimes doesn't pass authentication headers to
# FCGI instances. If your environment matches this description and you cannot
# authenticate, try this rule in your Apache setup:
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Basic
extend self
module ControllerMethods
def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure)
authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm)
end
def authenticate_with_http_basic(&login_procedure)
HttpAuthentication::Basic.authenticate(request, &login_procedure)
end
def request_http_basic_authentication(realm = "Application")
HttpAuthentication::Basic.authentication_request(self, realm)
end
end
def authenticate(request, &login_procedure)
unless authorization(request).blank?
login_procedure.call(*user_name_and_password(request))
end
end
def user_name_and_password(request)
decode_credentials(request).split(/:/, 2)
end
def authorization(request)
request.env['HTTP_AUTHORIZATION'] ||
request.env['X-HTTP_AUTHORIZATION'] ||
request.env['X_HTTP_AUTHORIZATION'] ||
request.env['REDIRECT_X_HTTP_AUTHORIZATION']
end
def decode_credentials(request)
ActiveSupport::Base64.decode64(authorization(request).split(' ', 2).last || '')
end
def encode_credentials(user_name, password)
"Basic #{ActiveSupport::Base64.encode64("#{user_name}:#{password}")}"
end
def authentication_request(controller, realm)
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}")
controller.response_body = "HTTP Basic: Access denied.\n"
controller.status = 401
end
end
module Digest
extend self
module ControllerMethods
def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm)
end
# Authenticate with HTTP Digest, returns true or false
def authenticate_with_http_digest(realm = "Application", &password_procedure)
HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
end
# Render output including the HTTP Digest authentication header
def request_http_digest_authentication(realm = "Application", message = nil)
HttpAuthentication::Digest.authentication_request(self, realm, message)
end
end
# Returns false on a valid response, true otherwise
def authenticate(request, realm, &password_procedure)
authorization(request) && validate_digest_response(request, realm, &password_procedure)
end
def authorization(request)
request.env['HTTP_AUTHORIZATION'] ||
request.env['X-HTTP_AUTHORIZATION'] ||
request.env['X_HTTP_AUTHORIZATION'] ||
request.env['REDIRECT_X_HTTP_AUTHORIZATION']
end
# Returns false unless the request credentials response value matches the expected value.
# First try the password as a ha1 digest password. If this fails, then try it as a plain
# text password.
def validate_digest_response(request, realm, &password_procedure)
credentials = decode_credentials_header(request)
valid_nonce = validate_nonce(request, credentials[:nonce])
if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque]
password = password_procedure.call(credentials[:username])
return false unless password
method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
uri = credentials[:uri][0,1] == '/' ? request.request_uri : request.url
[true, false].any? do |password_is_ha1|
expected = expected_response(method, uri, credentials, password, password_is_ha1)
expected == credentials[:response]
end
end
end
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
# Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
# of a plain-text password.
def expected_response(http_method, uri, credentials, password, password_is_ha1=true)
ha1 = password_is_ha1 ? password : ha1(credentials, password)
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':'))
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':'))
end
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
end
def encode_credentials(http_method, credentials, password, password_is_ha1)
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
"Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
end
def decode_credentials_header(request)
decode_credentials(authorization(request))
end
def decode_credentials(header)
header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
key, value = pair.split('=', 2)
hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
hash
end
end
def authentication_header(controller, realm)
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
end
def authentication_request(controller, realm, message = nil)
message ||= "HTTP Digest: Access denied.\n"
authentication_header(controller, realm)
controller.response_body = message
controller.status = 401
end
# Uses an MD5 digest based on time to generate a value to be used only once.
#
# A server-specified data string which should be uniquely generated each time a 401 response is made.
# It is recommended that this string be base64 or hexadecimal data.
# Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
#
# The contents of the nonce are implementation dependent.
# The quality of the implementation depends on a good choice.
# A nonce might, for example, be constructed as the base 64 encoding of
#
# => time-stamp H(time-stamp ":" ETag ":" private-key)
#
# where time-stamp is a server-generated time or other non-repeating value,
# ETag is the value of the HTTP ETag header associated with the requested entity,
# and private-key is data known only to the server.
# With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
# reject the request if it did not match the nonce from that header or
# if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
# The inclusion of the ETag prevents a replay request for an updated version of the resource.
# (Note: including the IP address of the client in the nonce would appear to offer the server the ability
# to limit the reuse of the nonce to the same client that originally got it.
# However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
# Also, IP address spoofing is not that hard.)
#
# An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
# POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
# of this document.
#
# The nonce is opaque to the client. Composed of Time, and hash of Time with secret
# key from the Rails session secret generated upon creation of project. Ensures
# the time cannot be modified by client.
def nonce(time = Time.now)
t = time.to_i
hashed = [t, secret_key]
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
ActiveSupport::Base64.encode64("#{t}:#{digest}").gsub("\n", '')
end
# Might want a shorter timeout depending on whether the request
# is a PUT or POST, and if client is browser or web service.
# Can be much shorter if the Stale directive is implemented. This would
# allow a user to use new nonce without prompting user again for their
# username and password.
def validate_nonce(request, value, seconds_to_timeout=5*60)
t = ActiveSupport::Base64.decode64(value).split(":").first.to_i
nonce(t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
end
# Opaque based on random generation - but changing each request?
def opaque()
::Digest::MD5.hexdigest(secret_key)
end
# Set in /initializers/session_store.rb, and loaded even if sessions are not in use.
def secret_key
ActionController::Base.session_options[:secret]
end
end
end
end
module ActionController
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
# repeated setups. The inclusion pattern has pages that look like this:
#
# <%= render "shared/header" %>
# Hello World
# <%= render "shared/footer" %>
#
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
#
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
# that the header and footer are only mentioned in one place, like this:
#
# // The header part of this layout
# <%= yield %>
# // The footer part of this layout
#
# And then you have content pages that look like this:
#
# hello world
#
# At rendering time, the content page is computed and then inserted in the layout, like this:
#
# // The header part of this layout
# hello world
# // The footer part of this layout
#
# == Accessing shared variables
#
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
# references that won't materialize before rendering time:
#
# <h1><%= @page_title %></h1>
# <%= yield %>
#
# ...and content pages that fulfill these references _at_ rendering time:
#
# <% @page_title = "Welcome" %>
# Off-world colonies offers you a chance to start a new life
#
# The result after rendering is:
#
# <h1>Welcome</h1>
# Off-world colonies offers you a chance to start a new life
#
# == Layout assignment
#
# You can either specify a layout declaratively (using the #layout class method) or give
# it the same name as your controller, and place it in <tt>app/views/layouts</tt>.
# If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance.
#
# For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>,
# that template will be used for all actions in PostsController and controllers inheriting
# from PostsController.
#
# If you use a module, for instance Weblog::PostsController, you will need a template named
# <tt>app/views/layouts/weblog/posts.html.erb</tt>.
#
# Since all your controllers inherit from ApplicationController, they will use
# <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified
# or provided.
#
# == Inheritance Examples
#
# class BankController < ActionController::Base
# layout "bank_standard"
#
# class InformationController < BankController
#
# class TellerController < BankController
# # teller.html.erb exists
#
# class TillController < TellerController
#
# class VaultController < BankController
# layout :access_level_layout
#
# class EmployeeController < BankController
# layout nil
#
# The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites
# and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
#
# The TellerController uses +teller.html.erb+, and TillController inherits that layout and
# uses it as well.
#
# == Types of layouts
#
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
#
# The method reference is the preferred approach to variable layouts and is used like this:
#
# class WeblogController < ActionController::Base
# layout :writers_and_readers
#
# def index
# # fetching posts
# end
#
# private
# def writers_and_readers
# logged_in? ? "writer_layout" : "reader_layout"
# end
#
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
# is logged in or not.
#
# If you want to use an inline method, such as a proc, do something like this:
#
# class WeblogController < ActionController::Base
# layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
#
# Of course, the most common way of specifying a layout is still just as a plain template name:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard"
#
# If no directory is specified for the template name, the template will by default be looked for in <tt>app/views/layouts/</tt>.
# Otherwise, it will be looked up relative to the template root.
#
# == Conditional layouts
#
# If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
# a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
# <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard", :except => :rss
#
# # ...
#
# end
#
# This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
# around the rendered view.
#
# Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
# #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>.
#
# == Using a different layout in the action render call
#
# If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
# Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller.
# You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard"
#
# def help
# render :action => "help", :layout => "help"
# end
# end
#
# This will render the help action with the "help" layout instead of the controller-wide "weblog_standard" layout.
module Layouts
extend ActiveSupport::Concern
include ActionController::RenderingController
include AbstractController::Layouts
module ClassMethods
# If no layout is provided, look for a layout with this name.
def _implied_layout_name
controller_path
end
end
end
end
module ActionController #:nodoc:
module MimeResponds #:nodoc:
extend ActiveSupport::Concern
included do
extlib_inheritable_accessor :responder, :mimes_for_respond_to, :instance_writer => false
self.responder = ActionController::Responder
clear_respond_to
end
module ClassMethods
# Defines mimes that are rendered by default when invoking respond_with.
#
# Examples:
#
# respond_to :html, :xml, :json
#
# All actions on your controller will respond to :html, :xml and :json.
#
# But if you want to specify it based on your actions, you can use only and
# except:
#
# respond_to :html
# respond_to :xml, :json, :except => [ :edit ]
#
# The definition above explicits that all actions respond to :html. And all
# actions except :edit respond to :xml and :json.
#
# You can specify also only parameters:
#
# respond_to :rjs, :only => :create
#
def respond_to(*mimes)
options = mimes.extract_options!
only_actions = Array(options.delete(:only))
except_actions = Array(options.delete(:except))
mimes.each do |mime|
mime = mime.to_sym
mimes_for_respond_to[mime] = {}
mimes_for_respond_to[mime][:only] = only_actions unless only_actions.empty?
mimes_for_respond_to[mime][:except] = except_actions unless except_actions.empty?
end
end
# Clear all mimes in respond_to.
#
def clear_respond_to
self.mimes_for_respond_to = ActiveSupport::OrderedHash.new
end
end
# Without web-service support, an action which collects the data for displaying a list of people
# might look something like this:
#
# def index
# @people = Person.find(:all)
# end
#
# Here's the same action, with web-service support baked in:
#
# def index
# @people = Person.find(:all)
#
# respond_to do |format|
# format.html
# format.xml { render :xml => @people.to_xml }
# end
# end
#
# What that says is, "if the client wants HTML in response to this action, just respond as we
# would have before, but if the client wants XML, return them the list of people in XML format."
# (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
#
# Supposing you have an action that adds a new person, optionally creating their company
# (by name) if it does not already exist, without web-services, it might look like this:
#
# def create
# @company = Company.find_or_create_by_name(params[:company][:name])
# @person = @company.people.create(params[:person])
#
# redirect_to(person_list_url)
# end
#
# Here's the same action, with web-service support baked in:
#
# def create
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by_name(company[:name])
# @person = @company.people.create(params[:person])
#
# respond_to do |format|
# format.html { redirect_to(person_list_url) }
# format.js
# format.xml { render :xml => @person.to_xml(:include => @company) }
# end
# end
#
# If the client wants HTML, we just redirect them back to the person list. If they want Javascript
# (format.js), then it is an RJS request and we render the RJS template associated with this action.
# Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
# include the person's company in the rendered XML, so you get something like this:
#
# <person>
# <id>...</id>
# ...
# <company>
# <id>...</id>
# <name>...</name>
# ...
# </company>
# </person>
#
# Note, however, the extra bit at the top of that action:
#
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by_name(company[:name])
#
# This is because the incoming XML document (if a web-service request is in process) can only contain a
# single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
#
# person[name]=...&person[company][name]=...&...
#
# And, like this (xml-encoded):
#
# <person>
# <name>...</name>
# <company>
# <name>...</name>
# </company>
# </person>
#
# In other words, we make the request so that it operates on a single entity's person. Then, in the action,
# we extract the company data from the request, find or create the company, and then create the new person
# with the remaining data.
#
# Note that you can define your own XML parameter parser which would allow you to describe multiple entities
# in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
# and accept Rails' defaults, life will be much easier.
#
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
# environment.rb as follows.
#
# Mime::Type.register "image/jpg", :jpg
#
# Respond to also allows you to specify a common block for different formats by using any:
#
# def index
# @people = Person.find(:all)
#
# respond_to do |format|
# format.html
# format.any(:xml, :json) { render request.format.to_sym => @people }
# end
# end
#
# In the example above, if the format is xml, it will render:
#
# render :xml => @people
#
# Or if the format is json:
#
# render :json => @people
#
# Since this is a common pattern, you can use the class method respond_to
# with the respond_with method to have the same results:
#
# class PeopleController < ApplicationController
# respond_to :html, :xml, :json
#
# def index
# @people = Person.find(:all)
# respond_with(@person)
# end
# end
#
# Be sure to check respond_with and respond_to documentation for more examples.
#
def respond_to(*mimes, &block)
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
if response = retrieve_response_from_mimes(mimes, &block)
response.call
end
end
# respond_with wraps a resource around a responder for default representation.
# First it invokes respond_to, if a response cannot be found (ie. no block
# for the request was given and template was not available), it instantiates
# an ActionController::Responder with the controller and resource.
#
# ==== Example
#
# def index
# @users = User.all
# respond_with(@users)
# end
#
# It also accepts a block to be given. It's used to overwrite a default
# response:
#
# def destroy
# @user = User.find(params[:id])
# flash[:notice] = "User was successfully created." if @user.save
#
# respond_with(@user) do |format|
# format.html { render }
# end
# end
#
# All options given to respond_with are sent to the underlying responder,
# except for the option :responder itself. Since the responder interface
# is quite simple (it just needs to respond to call), you can even give
# a proc to it.
#
def respond_with(*resources, &block)
if response = retrieve_response_from_mimes([], &block)
options = resources.extract_options!
options.merge!(:default_response => response)
(options.delete(:responder) || responder).call(self, resources, options)
end
end
protected
# Collect mimes declared in the class method respond_to valid for the
# current action.
#
def collect_mimes_from_class_level #:nodoc:
action = action_name.to_sym
mimes_for_respond_to.keys.select do |mime|
config = mimes_for_respond_to[mime]
if config[:except]
!config[:except].include?(action)
elsif config[:only]
config[:only].include?(action)
else
true
end
end
end
# Collects mimes and return the response for the negotiated format. Returns
# nil if :not_acceptable was sent to the client.
#
def retrieve_response_from_mimes(mimes, &block)
collector = Collector.new { default_render }
mimes = collect_mimes_from_class_level if mimes.empty?
mimes.each { |mime| collector.send(mime) }
block.call(collector) if block_given?
if format = request.negotiate_mime(collector.order)
self.formats = [format.to_sym]
collector.response_for(format)
else
head :not_acceptable
nil
end
end
class Collector #:nodoc:
attr_accessor :order
def initialize(&block)
@order, @responses, @default_response = [], {}, block
end
def any(*args, &block)
if args.any?
args.each { |type| send(type, &block) }
else
custom(Mime::ALL, &block)
end
end
alias :all :any
def custom(mime_type, &block)
mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
@order << mime_type
@responses[mime_type] ||= block
end
def response_for(mime)
@responses[mime] || @responses[Mime::ALL] || @default_response
end
def self.generate_method_for_mime(mime)
sym = mime.is_a?(Symbol) ? mime : mime.to_sym
const = sym.to_s.upcase
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{sym}(&block) # def html(&block)
custom(Mime::#{const}, &block) # custom(Mime::HTML, &block)
end # end
RUBY
end
Mime::SET.each do |mime|
generate_method_for_mime(mime)
end
def method_missing(symbol, &block)
mime_constant = Mime.const_get(symbol.to_s.upcase)
if Mime::SET.include?(mime_constant)
self.class.generate_method_for_mime(mime_constant)
send(symbol, &block)
else
super
end
end
end
end
end
module ActionController
module RackConvenience
extend ActiveSupport::Concern
included do
delegate :headers, :status=, :location=, :content_type=,
:status, :location, :content_type, :to => "@_response"
attr_internal :request
end
def dispatch(action, env)
@_request = ActionDispatch::Request.new(env)
@_response = ActionDispatch::Response.new
@_response.request = request
super
end
def params
@_params ||= @_request.parameters
end
def response_body=(body)
response.body = body if response
super
end
end
end
module ActionController
class RedirectBackError < AbstractController::Error #:nodoc:
DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].'
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
module Redirector
extend ActiveSupport::Concern
include AbstractController::Logger
def redirect_to(url, status) #:doc:
raise AbstractController::DoubleRenderError if response_body
logger.info("Redirected to #{url}") if logger && logger.info?
self.status = status
self.location = url.gsub(/[\r\n]/, '')
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(url)}\">redirected</a>.</body></html>"
end
end
end
module ActionController
module RenderOptions
extend ActiveSupport::Concern
included do
extlib_inheritable_accessor :_renderers
self._renderers = []
end
module ClassMethods
def _write_render_options
renderers = _renderers.map do |r|
<<-RUBY_EVAL
if options.key?(:#{r})
_process_options(options)
return render_#{r}(options[:#{r}], options)
end
RUBY_EVAL
end
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def _handle_render_options(options)
#{renderers.join}
end
RUBY_EVAL
end
def _add_render_option(name)
_renderers << name
_write_render_options
end
end
def render_to_body(options)
_handle_render_options(options) || super
end
end
module RenderOption #:nodoc:
def self.extended(base)
base.extend ActiveSupport::Concern
base.send :include, ::ActionController::RenderOptions
def base.register_renderer(name)
included { _add_render_option(name) }
end
end
end
module RenderOptions
module Json
extend RenderOption
register_renderer :json
def render_json(json, options)
json = ActiveSupport::JSON.encode(json) unless json.respond_to?(:to_str)
json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
self.content_type ||= Mime::JSON
self.response_body = json
end
end
module Js
extend RenderOption
register_renderer :js
def render_js(js, options)
self.content_type ||= Mime::JS
self.response_body = js.respond_to?(:to_js) ? js.to_js : js
end
end
module Xml
extend RenderOption
register_renderer :xml
def render_xml(xml, options)
self.content_type ||= Mime::XML
self.response_body = xml.respond_to?(:to_xml) ? xml.to_xml : xml
end
end
module RJS
extend RenderOption
register_renderer :update
def render_update(proc, options)
generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(view_context, &proc)
self.content_type = Mime::JS
self.response_body = generator.to_s
end
end
module All
extend ActiveSupport::Concern
include ActionController::RenderOptions::Json
include ActionController::RenderOptions::Js
include ActionController::RenderOptions::Xml
include ActionController::RenderOptions::RJS
end
end
end
module ActionController
module RenderingController
extend ActiveSupport::Concern
included do
include AbstractController::RenderingController
include AbstractController::LocalizedCache
end
def process_action(*)
self.formats = request.formats.map {|x| x.to_sym}
super
end
def render(options)
super
self.content_type ||= options[:_template].mime_type.to_s
response_body
end
def render_to_body(options)
_process_options(options)
if options.key?(:partial)
options[:partial] = action_name if options[:partial] == true
options[:_details] = {:formats => formats}
end
super
end
private
def _prefix
controller_path
end
def _determine_template(options)
if (options.keys & [:partial, :file, :template, :text, :inline]).empty?
options[:_template_name] ||= options[:action]
options[:_prefix] = _prefix
end
super
end
def format_for_text
formats.first
end
def _process_options(options)
status, content_type, location = options.values_at(:status, :content_type, :location)
self.status = status if status
self.content_type = content_type if content_type
self.headers["Location"] = url_for(location) if location
end
end
end
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
end
module RequestForgeryProtection
extend ActiveSupport::Concern
include AbstractController::Helpers, Session
included do
# Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
# sets it to <tt>:authenticity_token</tt> by default.
cattr_accessor :request_forgery_protection_token
# Controls whether request forgergy protection is turned on or not. Turned off by default only in test mode.
class_inheritable_accessor :allow_forgery_protection
self.allow_forgery_protection = true
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
# Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current
# web application, not a forged link from another site, is done by embedding a token based on a random
# string stored in the session (which an attacker wouldn't know) in all forms and Ajax requests generated
# by Rails and then verifying the authenticity of that token in the controller. Only HTML/JavaScript
# requests are checked, so this will not protect your XML API (presumably you'll have a different
# authentication scheme there anyway). Also, GET requests are not protected as these should be
# idempotent anyway.
#
# This is turned on with the <tt>protect_from_forgery</tt> method, which will check the token and raise an
# ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the
# error message in production by editing public/422.html. A call to this method in ApplicationController is
# generated by default in post-Rails 2.0 applications.
#
# The token parameter is named <tt>authenticity_token</tt> by default. If you are generating an HTML form
# manually (without the use of Rails' <tt>form_for</tt>, <tt>form_tag</tt> or other helpers), you have to
# include a hidden field named like that and set its value to what is returned by
# <tt>form_authenticity_token</tt>.
#
# Request forgery protection is disabled by default in test environment. If you are upgrading from Rails
# 1.x, add this to config/environments/test.rb:
#
# # Disable request forgery protection in test environment
# config.action_controller.allow_forgery_protection = false
#
# == Learn more about CSRF (Cross-Site Request Forgery) attacks
#
# Here are some resources:
# * http://isc.sans.org/diary.html?storyid=1750
# * http://en.wikipedia.org/wiki/Cross-site_request_forgery
#
# Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application.
# There are a few guidelines you should follow:
#
# * Keep your GET requests safe and idempotent. More reading material:
# * http://www.xml.com/pub/a/2002/04/24/deviant.html
# * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
# * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look
# for "Expires: at end of session"
#
module ClassMethods
# Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked.
#
# Example:
#
# class FooController < ApplicationController
# protect_from_forgery :except => :index
#
# # you can disable csrf protection on controller-by-controller basis:
# skip_before_filter :verify_authenticity_token
# end
#
# Valid Options:
#
# * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified.
def protect_from_forgery(options = {})
self.request_forgery_protection_token ||= :authenticity_token
before_filter :verify_authenticity_token, options
end
end
protected
# The actual before_filter that is used. Modify this to change how you handle unverified requests.
def verify_authenticity_token
verified_request? || raise(ActionController::InvalidAuthenticityToken)
end
# Returns true or false if a request is verified. Checks:
#
# * is the format restricted? By default, only HTML requests are checked.
# * is it a GET request? Gets should be safe and idempotent
# * Does the form_authenticity_token match the given token value from the params?
def verified_request?
!protect_against_forgery? || request.forgery_whitelisted? ||
form_authenticity_token == params[request_forgery_protection_token]
end
# Sets the token value for the current session.
def form_authenticity_token
session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
end
def protect_against_forgery?
allow_forgery_protection
end
end
end
module ActionController #:nodoc:
module Rescue
extend ActiveSupport::Concern
include ActiveSupport::Rescuable
private
def process_action(*args)
super
rescue Exception => exception
rescue_with_handler(exception) || raise(exception)
end
end
end
module ActionController #:nodoc:
# Responder is responsible to expose a resource for different mime requests,
# usually depending on the HTTP verb. The responder is triggered when
# respond_with is called. The simplest case to study is a GET request:
#
# class PeopleController < ApplicationController
# respond_to :html, :xml, :json
#
# def index
# @people = Person.find(:all)
# respond_with(@people)
# end
# end
#
# When a request comes, for example with format :xml, three steps happen:
#
# 1) responder searches for a template at people/index.xml;
#
# 2) if the template is not available, it will invoke :to_xml in the given resource;
#
# 3) if the responder does not respond_to :to_xml, call :to_format on it.
#
# === Builtin HTTP verb semantics
#
# Rails default responder holds semantics for each HTTP verb. Depending on the
# content type, verb and the resource status, it will behave differently.
#
# Using Rails default responder, a POST request for creating an object could
# be written as:
#
# def create
# @user = User.new(params[:user])
# flash[:notice] = 'User was successfully created.' if @user.save
# respond_with(@user)
# end
#
# Which is exactly the same as:
#
# def create
# @user = User.new(params[:user])
#
# respond_to do |format|
# if @user.save
# flash[:notice] = 'User was successfully created.'
# format.html { redirect_to(@user) }
# format.xml { render :xml => @user, :status => :created, :location => @user }
# else
# format.html { render :action => "new" }
# format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
# end
# end
# end
#
# The same happens for PUT and DELETE requests.
#
# === Nested resources
#
# You can given nested resource as you do in form_for and polymorphic_url.
# Consider the project has many tasks example. The create action for
# TasksController would be like:
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.comments.build(params[:task])
# flash[:notice] = 'Task was successfully created.' if @task.save
# respond_with(@project, @task)
# end
#
# Giving an array of resources, you ensure that the responder will redirect to
# project_task_url instead of task_url.
#
# Namespaced and singleton resources requires a symbol to be given, as in
# polymorphic urls. If a project has one manager which has many tasks, it
# should be invoked as:
#
# respond_with(@project, :manager, @task)
#
# Check polymorphic_url documentation for more examples.
#
class Responder
attr_reader :controller, :request, :format, :resource, :resources, :options
def initialize(controller, resources, options={})
@controller = controller
@request = controller.request
@format = controller.formats.first
@resource = resources.is_a?(Array) ? resources.last : resources
@resources = resources
@options = options
@action = options.delete(:action)
@default_response = options.delete(:default_response)
end
delegate :head, :render, :redirect_to, :to => :controller
delegate :get?, :post?, :put?, :delete?, :to => :request
# Undefine :to_json and :to_yaml since it's defined on Object
undef_method(:to_json) if method_defined?(:to_json)
undef_method(:to_yaml) if method_defined?(:to_yaml)
# Initializes a new responder an invoke the proper format. If the format is
# not defined, call to_format.
#
def self.call(*args)
responder = new(*args)
method = :"to_#{responder.format}"
responder.respond_to?(method) ? responder.send(method) : responder.to_format
end
# HTML format does not render the resource, it always attempt to render a
# template.
#
def to_html
default_render
rescue ActionView::MissingTemplate => e
navigation_behavior(e)
end
# All others formats follow the procedure below. First we try to render a
# template, if the template is not available, we verify if the resource
# responds to :to_format and display it.
#
def to_format
default_render
rescue ActionView::MissingTemplate => e
raise unless resourceful?
api_behavior(e)
end
protected
# This is the common behavior for "navigation" requests, like :html, :iphone and so forth.
def navigation_behavior(error)
if get?
raise error
elsif has_errors?
render :action => default_action
else
redirect_to resource_location
end
end
# This is the common behavior for "API" requests, like :xml and :json.
def api_behavior(error)
if get?
display resource
elsif has_errors?
display resource.errors, :status => :unprocessable_entity
elsif post?
display resource, :status => :created, :location => resource_location
else
head :ok
end
end
# Checks whether the resource responds to the current format or not.
#
def resourceful?
resource.respond_to?(:"to_#{format}")
end
# Returns the resource location by retrieving it from the options or
# returning the resources array.
#
def resource_location
options[:location] || resources
end
# If a given response block was given, use it, otherwise call render on
# controller.
#
def default_render
@default_response.call
end
# display is just a shortcut to render a resource with the current format.
#
# display @user, :status => :ok
#
# For xml request is equivalent to:
#
# render :xml => @user, :status => :ok
#
# Options sent by the user are also used:
#
# respond_with(@user, :status => :created)
# display(@user, :status => :ok)
#
# Results in:
#
# render :xml => @user, :status => :created
#
def display(resource, given_options={})
controller.render given_options.merge!(options).merge!(format => resource)
end
# Check if the resource has errors or not.
#
def has_errors?
resource.respond_to?(:errors) && !resource.errors.empty?
end
# By default, render the :edit action for html requests with failure, unless
# the verb is post.
#
def default_action
@action || (request.post? ? :new : :edit)
end
end
end
module ActionController
module Session
extend ActiveSupport::Concern
include RackConvenience
def session
@_request.session
end
def reset_session
@_request.reset_session
end
end
end
module ActionController #:nodoc:
module SessionManagement #:nodoc:
extend ActiveSupport::Concern
include ActionController::Configuration
module ClassMethods
# Set the session store to be used for keeping the session data between requests.
# By default, sessions are stored in browser cookies (<tt>:cookie_store</tt>),
# but you can also specify one of the other included stores (<tt>:active_record_store</tt>,
# <tt>:mem_cache_store</tt>, or your own custom class.
def session_store=(store)
if store == :active_record_store
self.session_store = ActiveRecord::SessionStore
else
@@session_store = store.is_a?(Symbol) ?
ActionDispatch::Session.const_get(store.to_s.camelize) :
store
end
end
# Returns the session store class currently used.
def session_store
if defined? @@session_store
@@session_store
else
ActionDispatch::Session::CookieStore
end
end
def session=(options = {})
self.session_store = nil if options.delete(:disabled)
session_options.merge!(options)
end
def session(*args)
ActiveSupport::Deprecation.warn(
"Disabling sessions for a single controller has been deprecated. " +
"Sessions are now lazy loaded. So if you don't access them, " +
"consider them off. You can still modify the session cookie " +
"options with request.session_options.", caller)
end
end
end
end
module ActionController #:nodoc:
# Methods for sending arbitrary data and for streaming files to the browser,
# instead of rendering.
module Streaming
extend ActiveSupport::Concern
include ActionController::RenderingController
DEFAULT_SEND_FILE_OPTIONS = {
:type => 'application/octet-stream'.freeze,
:disposition => 'attachment'.freeze,
:stream => true,
:buffer_size => 4096,
:x_sendfile => false
}.freeze
X_SENDFILE_HEADER = 'X-Sendfile'.freeze
protected
# Sends the file, by default streaming it 4096 bytes at a time. This way the
# whole file doesn't need to be read into memory at once. This makes it
# feasible to send even large files. You can optionally turn off streaming
# and send the whole file at once.
#
# Be careful to sanitize the path parameter if it is coming from a web
# page. <tt>send_file(params[:path])</tt> allows a malicious user to
# download any file on your server.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to <tt>File.basename(path)</tt>.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
# either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
# * <tt>:length</tt> - used to manually override the length (in bytes) of the content that
# is going to be sent to the client. Defaults to <tt>File.size(path)</tt>.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:stream</tt> - whether to send the file to the user agent as it is read (+true+)
# or to read the entire file before sending (+false+). Defaults to +true+.
# * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
# Defaults to 4096.
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
# * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from
# the URL, which is necessary for i18n filenames on certain browsers
# (setting <tt>:filename</tt> overrides this option).
# * <tt>:x_sendfile</tt> - uses X-Sendfile to send the file when set to +true+. This is currently
# only available with Lighttpd/Apache2 and specific modules installed and activated. Since this
# uses the web server to send the file, this may lower memory consumption on your server and
# it will not block your application for further requests.
# See http://blog.lighttpd.net/articles/2006/07/02/x-sendfile and
# http://tn123.ath.cx/mod_xsendfile/ for details. Defaults to +false+.
#
# The default Content-Type and Content-Disposition headers are
# set to download arbitrary binary files in as many browsers as
# possible. IE versions 4, 5, 5.5, and 6 are all known to have
# a variety of quirks (especially when downloading over SSL).
#
# Simple download:
#
# send_file '/path/to.zip'
#
# Show a JPEG in the browser:
#
# send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
#
# Show a 404 page in the browser:
#
# send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description) in
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
#
# Also be aware that the document may be cached by proxies and browsers.
# The Pragma and Cache-Control headers declare how the file may be cached
# by intermediaries. They default to require clients to validate with
# the server before releasing cached responses. See
# http://www.mnot.net/cache_docs/ for an overview of web caching and
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
# for the Cache-Control header spec.
def send_file(path, options = {}) #:doc:
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
options[:length] ||= File.size(path)
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
send_file_headers! options
@performed_render = false
if options[:x_sendfile]
logger.info "Sending #{X_SENDFILE_HEADER} header #{path}" if logger
head options[:status], X_SENDFILE_HEADER => path
else
if options[:stream]
# TODO : Make render :text => proc {} work with the new base
render :status => options[:status], :text => Proc.new { |response, output|
logger.info "Streaming file #{path}" unless logger.nil?
len = options[:buffer_size] || 4096
File.open(path, 'rb') do |file|
while buf = file.read(len)
output.write(buf)
end
end
}
else
logger.info "Sending file #{path}" unless logger.nil?
File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
end
end
end
# Sends the given binary data to the browser. This method is similar to
# <tt>render :text => data</tt>, but also allows you to specify whether
# the browser should display the response as a file attachment (i.e. in a
# download dialog) or as inline data. You may also set the content type,
# the apparent file name, and other things.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
# either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
#
# Generic data download:
#
# send_data buffer
#
# Download a dynamically-generated tarball:
#
# send_data generate_tgz('dir'), :filename => 'dir.tgz'
#
# Display an image Active Record in the browser:
#
# send_data image.data, :type => image.content_type, :disposition => 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
#
# <b>Tip:</b> if you want to stream large amounts of on-the-fly generated
# data to the browser, then use <tt>render :text => proc { ... }</tt>
# instead. See ActionController::Base#render for more information.
def send_data(data, options = {}) #:doc:
logger.info "Sending data #{options[:filename]}" if logger
send_file_headers! options.merge(:length => data.bytesize)
render :status => options[:status], :text => data
end
private
def send_file_headers!(options)
options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options))
[:length, :type, :disposition].each do |arg|
raise ArgumentError, ":#{arg} option required" if options[arg].nil?
end
disposition = options[:disposition].dup || 'attachment'
disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
content_type = options[:type]
if content_type.is_a?(Symbol)
raise ArgumentError, "Unknown MIME type #{options[:type]}" unless Mime::EXTENSION_LOOKUP.key?(content_type.to_s)
self.content_type = Mime::Type.lookup_by_extension(content_type.to_s)
else
self.content_type = content_type
end
headers.merge!(
'Content-Length' => options[:length].to_s,
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
)
response.sending_file = true
# Fix a problem with IE 6.0 on opening downloaded files:
# If Cache-Control: no-cache is set (which Rails does by default),
# IE removes the file it just downloaded from its cache immediately
# after it displays the "open/save" dialog, which means that if you
# hit "open" the file isn't there anymore when the application that
# is called for handling the download is run, so let's workaround that
response.cache_control[:public] ||= false
end
end
end
module ActionController
module Testing
extend ActiveSupport::Concern
include RackConvenience
# OMG MEGA HAX
def process_with_new_base_test(request, response)
@_request = request
@_response = response
@_response.request = request
ret = process(request.parameters[:action])
@_response.body ||= self.response_body
@_response.prepare!
set_test_assigns
ret
end
def set_test_assigns
@assigns = {}
(instance_variable_names - self.class.protected_instance_variables).each do |var|
name, value = var[1..-1], instance_variable_get(var)
@assigns[name] = value
end
end
# TODO : Rewrite tests using controller.headers= to use Rack env
def headers=(new_headers)
@_response ||= ActionDispatch::Response.new
@_response.headers.replace(new_headers)
end
module ClassMethods
def before_filters
_process_action_callbacks.find_all{|x| x.kind == :before}.map{|x| x.name}
end
end
end
end
module ActionController
module UrlFor
extend ActiveSupport::Concern
include RackConvenience
# Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in
# the form of a hash, just like the one you would use for url_for directly. Example:
#
# def default_url_options(options)
# { :project => @project.active? ? @project.url_name : "unknown" }
# end
#
# As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the
# urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set
# by this method.
def default_url_options(options = nil)
end
def rewrite_options(options) #:nodoc:
if defaults = default_url_options(options)
defaults.merge(options)
else
options
end
end
def url_for(options = {})
options ||= {}
case options
when String
options
when Hash
@url ||= UrlRewriter.new(request, params)
@url.rewrite(rewrite_options(options))
else
polymorphic_url(options)
end
end
end
end
module ActionController #:nodoc:
module Verification #:nodoc:
extend ActiveSupport::Concern
include AbstractController::Callbacks, Session, Flash, RenderingController
# This module provides a class-level method for specifying that certain
# actions are guarded against being called without certain prerequisites
# being met. This is essentially a special kind of before_filter.
#
# An action may be guarded against being invoked without certain request
# parameters being set, or without certain session values existing.
#
# When a verification is violated, values may be inserted into the flash, and
# a specified redirection is triggered. If no specific action is configured,
# verification failures will by default result in a 400 Bad Request response.
#
# Usage:
#
# class GlobalController < ActionController::Base
# # Prevent the #update_settings action from being invoked unless
# # the 'admin_privileges' request parameter exists. The
# # settings action will be redirected to in current controller
# # if verification fails.
# verify :params => "admin_privileges", :only => :update_post,
# :redirect_to => { :action => "settings" }
#
# # Disallow a post from being updated if there was no information
# # submitted with the post, and if there is no active post in the
# # session, and if there is no "note" key in the flash. The route
# # named category_url will be redirected to if verification fails.
#
# verify :params => "post", :session => "post", "flash" => "note",
# :only => :update_post,
# :add_flash => { "alert" => "Failed to create your message" },
# :redirect_to => :category_url
#
# Note that these prerequisites are not business rules. They do not examine
# the content of the session or the parameters. That level of validation should
# be encapsulated by your domain model or helper methods in the controller.
module ClassMethods
# Verify the given actions so that if certain prerequisites are not met,
# the user is redirected to a different action. The +options+ parameter
# is a hash consisting of the following key/value pairs:
#
# <tt>:params</tt>::
# a single key or an array of keys that must be in the <tt>params</tt>
# hash in order for the action(s) to be safely called.
# <tt>:session</tt>::
# a single key or an array of keys that must be in the <tt>session</tt>
# in order for the action(s) to be safely called.
# <tt>:flash</tt>::
# a single key or an array of keys that must be in the flash in order
# for the action(s) to be safely called.
# <tt>:method</tt>::
# a single key or an array of keys--any one of which must match the
# current request method in order for the action(s) to be safely called.
# (The key should be a symbol: <tt>:get</tt> or <tt>:post</tt>, for
# example.)
# <tt>:xhr</tt>::
# true/false option to ensure that the request is coming from an Ajax
# call or not.
# <tt>:add_flash</tt>::
# a hash of name/value pairs that should be merged into the session's
# flash if the prerequisites cannot be satisfied.
# <tt>:add_headers</tt>::
# a hash of name/value pairs that should be merged into the response's
# headers hash if the prerequisites cannot be satisfied.
# <tt>:redirect_to</tt>::
# the redirection parameters to be used when redirecting if the
# prerequisites cannot be satisfied. You can redirect either to named
# route or to the action in some controller.
# <tt>:render</tt>::
# the render parameters to be used when the prerequisites cannot be satisfied.
# <tt>:only</tt>::
# only apply this verification to the actions specified in the associated
# array (may also be a single value).
# <tt>:except</tt>::
# do not apply this verification to the actions specified in the associated
# array (may also be a single value).
def verify(options={})
before_filter :only => options[:only], :except => options[:except] do
verify_action options
end
end
end
private
def verify_action(options) #:nodoc:
if prereqs_invalid?(options)
flash.update(options[:add_flash]) if options[:add_flash]
response.headers.merge!(options[:add_headers]) if options[:add_headers]
apply_remaining_actions(options) unless performed?
end
end
def prereqs_invalid?(options) # :nodoc:
verify_presence_of_keys_in_hash_flash_or_params(options) ||
verify_method(options) ||
verify_request_xhr_status(options)
end
def verify_presence_of_keys_in_hash_flash_or_params(options) # :nodoc:
[*options[:params] ].find { |v| v && params[v.to_sym].nil? } ||
[*options[:session]].find { |v| session[v].nil? } ||
[*options[:flash] ].find { |v| flash[v].nil? }
end
def verify_method(options) # :nodoc:
[*options[:method]].all? { |v| request.method != v.to_sym } if options[:method]
end
def verify_request_xhr_status(options) # :nodoc:
request.xhr? != options[:xhr] unless options[:xhr].nil?
end
def apply_redirect_to(redirect_to_option) # :nodoc:
(redirect_to_option.is_a?(Symbol) && redirect_to_option != :back) ? self.__send__(redirect_to_option) : redirect_to_option
end
def apply_remaining_actions(options) # :nodoc:
case
when options[:render] ; render(options[:render])
when options[:redirect_to] ; redirect_to(apply_redirect_to(options[:redirect_to]))
else head(:bad_request)
end
end
end
end
require 'active_support/core_ext/class/inheritable_attributes'
module ActionController
# ActionController::Metal provides a way to get a valid Rack application from a controller.
#
# In AbstractController, dispatching is triggered directly by calling #process on a new controller.
# ActionController::Metal provides an #action method that returns a valid Rack application for a
# given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails router,
# can dispatch directly to the action returned by FooController.action(:index).
class Metal < AbstractController::Base
abstract!
# :api: public
attr_internal :params, :env
# Returns the last part of the controller's name, underscored, without the ending
# "Controller". For instance, MyApp::MyPostsController would return "my_posts" for
# controller_name
#
# ==== Returns
# String
def self.controller_name
@controller_name ||= controller_path.split("/").last
end
# Delegates to the class' #controller_name
def controller_name
self.class.controller_name
end
# Returns the full controller name, underscored, without the ending Controller.
# For instance, MyApp::MyPostsController would return "my_app/my_posts" for
# controller_name.
#
# ==== Returns
# String
def self.controller_path
@controller_path ||= name && name.sub(/Controller$/, '').underscore
end
# Delegates to the class' #controller_path
def controller_path
self.class.controller_path
end
# The details below can be overridden to support a specific
# Request and Response object. The default ActionController::Base
# implementation includes RackConvenience, which makes a request
# and response object available. You might wish to control the
# environment and response manually for performance reasons.
attr_internal :status, :headers, :content_type, :response
def initialize(*)
@_headers = {}
super
end
# Basic implementations for content_type=, location=, and headers are
# provided to reduce the dependency on the RackConvenience module
# in Renderer and Redirector.
def content_type=(type)
headers["Content-Type"] = type.to_s
end
def location=(url)
headers["Location"] = url
end
# :api: private
def dispatch(name, env)
@_env = env
process(name)
to_a
end
# :api: private
def to_a
response ? response.to_a : [status, headers, response_body]
end
class ActionEndpoint
@@endpoints = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = {} } }
def self.for(controller, action, stack)
@@endpoints[controller][action][stack] ||= begin
endpoint = new(controller, action)
stack.build(endpoint)
end
end
def initialize(controller, action)
@controller, @action = controller, action
end
def call(env)
@controller.new.dispatch(@action, env)
end
end
extlib_inheritable_accessor(:middleware_stack) { ActionDispatch::MiddlewareStack.new }
def self.use(*args)
middleware_stack.use(*args)
end
def self.middleware
middleware_stack
end
def self.call(env)
action(env['action_dispatch.request.path_parameters'][:action]).call(env)
end
# Return a rack endpoint for the given action. Memoize the endpoint, so
# multiple calls into MyController.action will return the same object
# for the same action.
#
# ==== Parameters
# action<#to_s>:: An action name
#
# ==== Returns
# Proc:: A rack application
def self.action(name)
ActionEndpoint.for(self, name, middleware_stack)
end
end
end
module ActionController
class Middleware < Metal
class ActionMiddleware
def initialize(controller, app)
@controller, @app = controller, app
end
def call(env)
@controller.build(@app).dispatch(:index, env)
end
end
class << self
alias build new
def new(app)
ActionMiddleware.new(self, app)
end
end
attr_internal :app
def process(action)
response = super
self.status, self.headers, self.response_body = response if response.is_a?(Array)
response
end
def initialize(app)
super()
@_app = app
end
def index
call(env)
end
end
endrequire 'active_support/notifications'
ActiveSupport::Notifications.subscribe(/(read|write|cache|expire|exist)_(fragment|page)\??/) do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
if logger = ActionController::Base.logger
human_name = event.name.to_s.humanize
logger.info("#{human_name} (%.1fms)" % event.duration)
end
end
module ActionController
# Polymorphic URL helpers are methods for smart resolution to a named route call when
# given an Active Record model instance. They are to be used in combination with
# ActionController::Resources.
#
# These methods are useful when you want to generate correct URL or path to a RESTful
# resource without having to know the exact type of the record in question.
#
# Nested resources and/or namespaces are also supported, as illustrated in the example:
#
# polymorphic_url([:admin, @article, @comment])
#
# results in:
#
# admin_article_comment_url(@article, @comment)
#
# == Usage within the framework
#
# Polymorphic URL helpers are used in a number of places throughout the Rails framework:
#
# * <tt>url_for</tt>, so you can use it with a record as the argument, e.g.
# <tt>url_for(@article)</tt>;
# * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write
# <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form
# action;
# * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write
# <tt>redirect_to(post)</tt> in your controllers;
# * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs
# for feed entries.
#
# == Prefixed polymorphic helpers
#
# In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a
# number of prefixed helpers are available as a shorthand to <tt>:action => "..."</tt>
# in options. Those are:
#
# * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
# * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt>
#
# Example usage:
#
# edit_polymorphic_path(@post) # => "/posts/1/edit"
# polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf"
module PolymorphicRoutes
# Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example:
#
# # calls post_url(post)
# polymorphic_url(post) # => "http://example.com/posts/1"
# polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1"
# polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1"
# polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1"
# polymorphic_url(Comment) # => "http://example.com/comments"
#
# ==== Options
#
# * <tt>:action</tt> - Specifies the action prefix for the named route:
# <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix.
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
# Default is <tt>:url</tt>.
#
# ==== Examples
#
# # an Article record
# polymorphic_url(record) # same as article_url(record)
#
# # a Comment record
# polymorphic_url(record) # same as comment_url(record)
#
# # it recognizes new records and maps to the collection
# record = Comment.new
# polymorphic_url(record) # same as comments_url()
#
# # the class of a record will also map to the collection
# polymorphic_url(Comment) # same as comments_url()
#
def polymorphic_url(record_or_hash_or_array, options = {})
if record_or_hash_or_array.kind_of?(Array)
record_or_hash_or_array = record_or_hash_or_array.compact
record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1
end
record = extract_record(record_or_hash_or_array)
record = record.to_model if record.respond_to?(:to_model)
args = case record_or_hash_or_array
when Hash; [ record_or_hash_or_array ]
when Array; record_or_hash_or_array.dup
else [ record_or_hash_or_array ]
end
inflection = if options[:action].to_s == "new"
args.pop
:singular
elsif (record.respond_to?(:new_record?) && record.new_record?) ||
(record.respond_to?(:destroyed?) && record.destroyed?)
args.pop
:plural
elsif record.is_a?(Class)
args.pop
:plural
else
:singular
end
args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
named_route = build_named_route_call(record_or_hash_or_array, inflection, options)
url_options = options.except(:action, :routing_type)
unless url_options.empty?
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
end
__send__(named_route, *args)
end
# Returns the path component of a URL for the given record. It uses
# <tt>polymorphic_url</tt> with <tt>:routing_type => :path</tt>.
def polymorphic_path(record_or_hash_or_array, options = {})
polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path))
end
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__
def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
polymorphic_url( # polymorphic_url(
record_or_hash, # record_or_hash,
options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
end # end
#
def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
polymorphic_url( # polymorphic_url(
record_or_hash, # record_or_hash,
options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
end # end
EOT
end
private
def action_prefix(options)
options[:action] ? "#{options[:action]}_" : ''
end
def routing_type(options)
options[:routing_type] || :url
end
def build_named_route_call(records, inflection, options = {})
unless records.is_a?(Array)
record = extract_record(records)
route = ''
else
record = records.pop
route = records.inject("") do |string, parent|
if parent.is_a?(Symbol) || parent.is_a?(String)
string << "#{parent}_"
else
string << RecordIdentifier.__send__("plural_class_name", parent).singularize
string << "_"
end
end
end
if record.is_a?(Symbol) || record.is_a?(String)
route << "#{record}_"
else
route << RecordIdentifier.__send__("plural_class_name", record)
route = route.singularize if inflection == :singular
route << "_"
end
action_prefix(options) + route + routing_type(options).to_s
end
def extract_record(record_or_hash_or_array)
case record_or_hash_or_array
when Array; record_or_hash_or_array.last
when Hash; record_or_hash_or_array[:id]
else record_or_hash_or_array
end
end
end
end
require 'active_support/core_ext/module'
module ActionController
# The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or
# Active Resources or pretty much any other model type that has an id. These patterns are then used to try elevate
# the view actions to a higher logical level. Example:
#
# # routes
# map.resources :posts
#
# # view
# <% div_for(post) do %> <div id="post_45" class="post">
# <%= post.body %> What a wonderful world!
# <% end %> </div>
#
# # controller
# def destroy
# post = Post.find(params[:id])
# post.destroy
#
# respond_to do |format|
# format.html { redirect_to(post) } # Calls polymorphic_url(post) which in turn calls post_url(post)
# format.js do
# # Calls: new Effect.fade('post_45');
# render(:update) { |page| page[post].visual_effect(:fade) }
# end
# end
# end
#
# As the example above shows, you can stop caring to a large extent what the actual id of the post is. You just know
# that one is being assigned and that the subsequent calls in redirect_to and the RJS expect that same naming
# convention and allows you to write less code if you follow it.
module RecordIdentifier
extend self
JOIN = '_'.freeze
NEW = 'new'.freeze
# The DOM class convention is to use the singular form of an object or class. Examples:
#
# dom_class(post) # => "post"
# dom_class(Person) # => "person"
#
# If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
#
# dom_class(post, :edit) # => "edit_post"
# dom_class(Person, :edit) # => "edit_person"
def dom_class(record_or_class, prefix = nil)
singular = singular_class_name(record_or_class)
prefix ? "#{prefix}#{JOIN}#{singular}" : singular
end
# The DOM id convention is to use the singular form of an object or class with the id following an underscore.
# If no id is found, prefix with "new_" instead. Examples:
#
# dom_id(Post.find(45)) # => "post_45"
# dom_id(Post.new) # => "new_post"
#
# If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
#
# dom_id(Post.find(45), :edit) # => "edit_post_45"
def dom_id(record, prefix = nil)
if record_id = record.id
"#{dom_class(record, prefix)}#{JOIN}#{record_id}"
else
dom_class(record, prefix || NEW)
end
end
# Returns the plural class name of a record or class. Examples:
#
# plural_class_name(post) # => "posts"
# plural_class_name(Highrise::Person) # => "highrise_people"
def plural_class_name(record_or_class)
model_name_from_record_or_class(record_or_class).plural
end
# Returns the singular class name of a record or class. Examples:
#
# singular_class_name(post) # => "post"
# singular_class_name(Highrise::Person) # => "highrise_person"
def singular_class_name(record_or_class)
model_name_from_record_or_class(record_or_class).singular
end
private
def model_name_from_record_or_class(record_or_class)
(record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name
end
end
end
require 'active_support/core_ext/object/conversions'
require "rack/test"
module ActionController #:nodoc:
# Essentially generates a modified Tempfile object similar to the object
# you'd get from the standard library CGI module in a multipart
# request. This means you can use an ActionController::TestUploadedFile
# object in the params of a test request in order to simulate
# a file upload.
#
# Usage example, within a functional test:
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
#
# Pass a true third parameter to ensure the uploaded file is opened in binary mode (only required for Windows):
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + '/files/spongebob.png', 'image/png', :binary)
TestUploadedFile = Rack::Test::UploadedFile
module TestProcess
def assigns(key = nil)
assigns = {}
@controller.instance_variable_names.each do |ivar|
next if ActionController::Base.protected_instance_variables.include?(ivar)
assigns[ivar[1..-1]] = @controller.instance_variable_get(ivar)
end
key.nil? ? assigns : assigns[key.to_s]
end
def session
@request.session
end
def flash
@request.flash
end
def cookies
@request.cookies.merge(@response.cookies)
end
def redirect_to_url
@response.redirect_url
end
def html_document
xml = @response.content_type =~ /xml$/
@html_document ||= HTML::Document.new(@response.body, false, xml)
end
def find_tag(conditions)
html_document.find(conditions)
end
def find_all_tag(conditions)
html_document.find_all(conditions)
end
def method_missing(selector, *args, &block)
if @controller && ActionController::Routing::Routes.named_routes.helpers.include?(selector)
@controller.send(selector, *args, &block)
else
super
end
end
# Shortcut for <tt>ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + path, type)</tt>:
#
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
#
# To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
# This will not affect other platforms:
#
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary)
def fixture_file_upload(path, mime_type = nil, binary = false)
fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path)
ActionController::TestUploadedFile.new("#{fixture_path}#{path}", mime_type, binary)
end
# A helper to make it easier to test different route configurations.
# This method temporarily replaces ActionController::Routing::Routes
# with a new RouteSet instance.
#
# The new instance is yielded to the passed block. Typically the block
# will create some routes using <tt>map.draw { map.connect ... }</tt>:
#
# with_routing do |set|
# set.draw do |map|
# map.connect ':controller/:action/:id'
# assert_equal(
# ['/content/10/show', {}],
# map.generate(:controller => 'content', :id => 10, :action => 'show')
# end
# end
# end
#
def with_routing
real_routes = ActionController::Routing::Routes
ActionController::Routing.module_eval { remove_const :Routes }
temporary_routes = ActionController::Routing::RouteSet.new
ActionController::Routing.module_eval { const_set :Routes, temporary_routes }
yield temporary_routes
ensure
if ActionController::Routing.const_defined? :Routes
ActionController::Routing.module_eval { remove_const :Routes }
end
ActionController::Routing.const_set(:Routes, real_routes) if real_routes
end
end
end
require 'active_support/test_case'
require 'rack/session/abstract/id'
module ActionController
class TestRequest < ActionDispatch::TestRequest #:nodoc:
def initialize(env = {})
super
self.session = TestSession.new
self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => ActiveSupport::SecureRandom.hex(16))
end
class Result < ::Array #:nodoc:
def to_s() join '/' end
def self.new_escaped(strings)
new strings.collect {|str| URI.unescape str}
end
end
def assign_parameters(controller_path, action, parameters = {})
parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
non_path_parameters = get? ? query_parameters : request_parameters
parameters.each do |key, value|
if value.is_a? Fixnum
value = value.to_s
elsif value.is_a? Array
value = Result.new(value)
end
if extra_keys.include?(key.to_sym)
non_path_parameters[key] = value
else
path_parameters[key.to_s] = value
end
end
params = self.request_parameters.dup
%w(controller action only_path).each do |k|
params.delete(k)
params.delete(k.to_sym)
end
data = params.to_query
@env['CONTENT_LENGTH'] = data.length.to_s
@env['rack.input'] = StringIO.new(data)
end
def recycle!
@formats = nil
@env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
@env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
@env['action_dispatch.request.query_parameters'] = {}
end
end
class TestResponse < ActionDispatch::TestResponse
def recycle!
@status = 200
@header = {}
@writer = lambda { |x| @body << x }
@block = nil
@length = 0
@body = []
@charset = nil
@content_type = nil
@request = @template = nil
end
end
class TestSession < ActionDispatch::Session::AbstractStore::SessionHash #:nodoc:
DEFAULT_OPTIONS = ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS
def initialize(session = {})
replace(session.stringify_keys)
@loaded = true
end
end
# Superclass for ActionController functional tests. Functional tests allow you to
# test a single controller action per test method. This should not be confused with
# integration tests (see ActionController::IntegrationTest), which are more like
# "stories" that can involve multiple controllers and mutliple actions (i.e. multiple
# different HTTP requests).
#
# == Basic example
#
# Functional tests are written as follows:
# 1. First, one uses the +get+, +post+, +put+, +delete+ or +head+ method to simulate
# an HTTP request.
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
# the controller's HTTP response, the database contents, etc.
#
# For example:
#
# class BooksControllerTest < ActionController::TestCase
# def test_create
# # Simulate a POST response with the given HTTP parameters.
# post(:create, :book => { :title => "Love Hina" })
#
# # Assert that the controller tried to redirect us to
# # the created book's URI.
# assert_response :found
#
# # Assert that the controller really put the book in the database.
# assert_not_nil Book.find_by_title("Love Hina")
# end
# end
#
# == Special instance variables
#
# ActionController::TestCase will also automatically provide the following instance
# variables for use in the tests:
#
# <b>@controller</b>::
# The controller instance that will be tested.
# <b>@request</b>::
# An ActionController::TestRequest, representing the current HTTP
# request. You can modify this object before sending the HTTP request. For example,
# you might want to set some session properties before sending a GET request.
# <b>@response</b>::
# An ActionController::TestResponse object, representing the response
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
# after calling +post+. If the various assert methods are not sufficient, then you
# may use this object to inspect the HTTP response in detail.
#
# (Earlier versions of Rails required each functional test to subclass
# Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
#
# == Controller is automatically inferred
#
# ActionController::TestCase will automatically infer the controller under test
# from the test class name. If the controller cannot be inferred from the test
# class name, you can explicitly set it with +tests+.
#
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
# tests WidgetController
# end
#
# == Testing controller internals
#
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
# For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
# appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing.
# So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
#
# On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
# @request.cookies["key"] = "value"
#
# == Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
# Example:
#
# assert_redirected_to page_url(:title => 'foo')
class TestCase < ActiveSupport::TestCase
include TestProcess
# Executes a request simulating GET HTTP method and set/volley the response
def get(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "GET")
end
# Executes a request simulating POST HTTP method and set/volley the response
def post(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "POST")
end
# Executes a request simulating PUT HTTP method and set/volley the response
def put(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "PUT")
end
# Executes a request simulating DELETE HTTP method and set/volley the response
def delete(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "DELETE")
end
# Executes a request simulating HEAD HTTP method and set/volley the response
def head(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "HEAD")
end
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
@request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
returning __send__(request_method, action, parameters, session, flash) do
@request.env.delete 'HTTP_X_REQUESTED_WITH'
@request.env.delete 'HTTP_ACCEPT'
end
end
alias xhr :xml_http_request
def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
# Sanity check for required instance variables so we can give an
# understandable error message.
%w(@controller @request @response).each do |iv_name|
if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil?
raise "#{iv_name} is nil: make sure you set it in your test's setup method."
end
end
@request.recycle!
@response.recycle!
@controller.response_body = nil
@controller.formats = nil
@controller.params = nil
@html_document = nil
@request.env['REQUEST_METHOD'] = http_method
parameters ||= {}
@request.assign_parameters(@controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters)
@request.session = ActionController::TestSession.new(session) unless session.nil?
@request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
@controller.request = @request
@controller.params.merge!(parameters)
build_request_uri(action, parameters)
Base.class_eval { include Testing }
@controller.process_with_new_base_test(@request, @response)
@response
end
include ActionDispatch::Assertions
# When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
# (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
# rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else
# than 0.0.0.0.
#
# The exception is stored in the exception accessor for further inspection.
module RaiseActionExceptions
def self.included(base)
base.class_eval do
attr_accessor :exception
protected :exception, :exception=
end
end
protected
def rescue_action_without_handler(e)
self.exception = e
if request.remote_addr == "0.0.0.0"
raise(e)
else
super(e)
end
end
end
setup :setup_controller_request_and_response
@@controller_class = nil
class << self
# Sets the controller class name. Useful if the name can't be inferred from test class.
# Expects +controller_class+ as a constant. Example: <tt>tests WidgetController</tt>.
def tests(controller_class)
self.controller_class = controller_class
end
def controller_class=(new_class)
prepare_controller_class(new_class) if new_class
write_inheritable_attribute(:controller_class, new_class)
end
def controller_class
if current_controller_class = read_inheritable_attribute(:controller_class)
current_controller_class
else
self.controller_class = determine_default_controller_class(name)
end
end
def determine_default_controller_class(name)
name.sub(/Test$/, '').constantize
rescue NameError
nil
end
def prepare_controller_class(new_class)
new_class.send :include, RaiseActionExceptions
end
end
def setup_controller_request_and_response
@request = TestRequest.new
@response = TestResponse.new
if klass = self.class.controller_class
@controller ||= klass.new rescue nil
end
if @controller
@controller.request = @request
@controller.params = {}
end
end
# Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local
def rescue_action_in_public!
@request.remote_addr = '208.77.188.166' # example.com
end
private
def build_request_uri(action, parameters)
unless @request.env['REQUEST_URI']
options = @controller.__send__(:rewrite_options, parameters)
options.update(:only_path => true, :action => action)
url = ActionController::UrlRewriter.new(@request, parameters)
@request.request_uri = url.rewrite(options)
end
end
end
end
module ActionController
module Translation
def translate(*args)
I18n.translate(*args)
end
alias :t :translate
def localize(*args)
I18n.localize(*args)
end
alias :l :localize
end
endmodule ActionController
# In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse
# is also possible: an URL can be generated from one of your routing definitions.
# URL generation functionality is centralized in this module.
#
# See ActionController::Routing and ActionController::Resources for general
# information about routing and routes.rb.
#
# <b>Tip:</b> If you need to generate URLs from your models or some other place,
# then ActionController::UrlWriter is what you're looking for. Read on for
# an introduction.
#
# == URL generation from parameters
#
# As you may know, some functions - such as ActionController::Base#url_for
# and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
# of parameters. For example, you've probably had the chance to write code
# like this in one of your views:
#
# <%= link_to('Click here', :controller => 'users',
# :action => 'new', :message => 'Welcome!') %>
#
# #=> Generates a link to: /users/new?message=Welcome%21
#
# link_to, and all other functions that require URL generation functionality,
# actually use ActionController::UrlWriter under the hood. And in particular,
# they use the ActionController::UrlWriter#url_for method. One can generate
# the same path as the above example by using the following code:
#
# include UrlWriter
# url_for(:controller => 'users',
# :action => 'new',
# :message => 'Welcome!',
# :only_path => true)
# # => "/users/new?message=Welcome%21"
#
# Notice the <tt>:only_path => true</tt> part. This is because UrlWriter has no
# information about the website hostname that your Rails app is serving. So if you
# want to include the hostname as well, then you must also pass the <tt>:host</tt>
# argument:
#
# include UrlWriter
# url_for(:controller => 'users',
# :action => 'new',
# :message => 'Welcome!',
# :host => 'www.example.com') # Changed this.
# # => "http://www.example.com/users/new?message=Welcome%21"
#
# By default, all controllers and views have access to a special version of url_for,
# that already knows what the current hostname is. So if you use url_for in your
# controllers or your views, then you don't need to explicitly pass the <tt>:host</tt>
# argument.
#
# For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for.
# So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for'
# in full. However, mailers don't have hostname information, and what's why you'll still
# have to specify the <tt>:host</tt> argument when generating URLs in mailers.
#
#
# == URL generation for named routes
#
# UrlWriter also allows one to access methods that have been auto-generated from
# named routes. For example, suppose that you have a 'users' resource in your
# <b>routes.rb</b>:
#
# map.resources :users
#
# This generates, among other things, the method <tt>users_path</tt>. By default,
# this method is accessible from your controllers, views and mailers. If you need
# to access this auto-generated method from other places (such as a model), then
# you can do that by including ActionController::UrlWriter in your class:
#
# class User < ActiveRecord::Base
# include ActionController::UrlWriter
#
# def base_uri
# user_path(self)
# end
# end
#
# User.find(1).base_uri # => "/users/1"
module UrlWriter
def self.included(base) #:nodoc:
ActionController::Routing::Routes.install_helpers(base)
base.mattr_accessor :default_url_options
# The default options for urls written by this writer. Typically a <tt>:host</tt> pair is provided.
base.default_url_options ||= {}
end
# Generate a url based on the options provided, default_url_options and the
# routes defined in routes.rb. The following options are supported:
#
# * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+.
# * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'.
# * <tt>:host</tt> - Specifies the host the link should be targeted at.
# If <tt>:only_path</tt> is false, this option must be
# provided either explicitly, or via +default_url_options+.
# * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
# * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the
# +relative_url_root+ set in ActionController::Base.relative_url_root.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
#
# Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
# +url_for+ is forwarded to the Routes module.
#
# Examples:
#
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing'
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok'
# url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/'
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33'
def url_for(options)
options = self.class.default_url_options.merge(options)
url = ''
unless options.delete(:only_path)
url << (options.delete(:protocol) || 'http')
url << '://' unless url.match("://")
raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host]
url << options.delete(:host)
url << ":#{options.delete(:port)}" if options.key?(:port)
else
# Delete the unused options to prevent their appearance in the query string.
[:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) }
end
trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash)
url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor]
generated = Routing::Routes.generate(options, {})
url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated)
url << anchor if anchor
url
end
end
# Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
class UrlRewriter #:nodoc:
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root]
def initialize(request, parameters)
@request, @parameters = request, parameters
end
def rewrite(options = {})
rewrite_url(options)
end
def to_str
"#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}"
end
alias_method :to_s, :to_str
private
# Given a path and options, returns a rewritten URL string
def rewrite_url(options)
rewritten_url = ""
unless options[:only_path]
rewritten_url << (options[:protocol] || @request.protocol)
rewritten_url << "://" unless rewritten_url.match("://")
rewritten_url << rewrite_authentication(options)
rewritten_url << (options[:host] || @request.host_with_port)
rewritten_url << ":#{options.delete(:port)}" if options.key?(:port)
end
path = rewrite_path(options)
rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor]
rewritten_url
end
# Given a Hash of options, generates a route
def rewrite_path(options)
options = options.symbolize_keys
options.update(options[:params].symbolize_keys) if options[:params]
if (overwrite = options.delete(:overwrite_params))
options.update(@parameters.symbolize_keys)
options.update(overwrite.symbolize_keys)
end
RESERVED_OPTIONS.each { |k| options.delete(k) }
# Generates the query string, too
Routing::Routes.generate(options, @request.symbolized_path_parameters)
end
def rewrite_authentication(options)
if options[:user] && options[:password]
"#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@"
else
""
end
end
end
end
require 'html/tokenizer'
require 'html/node'
require 'html/selector'
require 'html/sanitizer'
module HTML #:nodoc:
# A top-level HTMl document. You give it a body of text, and it will parse that
# text into a tree of nodes.
class Document #:nodoc:
# The root of the parsed document.
attr_reader :root
# Create a new Document from the given text.
def initialize(text, strict=false, xml=false)
tokenizer = Tokenizer.new(text)
@root = Node.new(nil)
node_stack = [ @root ]
while token = tokenizer.next
node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict)
node_stack.last.children << node unless node.tag? && node.closing == :close
if node.tag?
if node_stack.length > 1 && node.closing == :close
if node_stack.last.name == node.name
if node_stack.last.children.empty?
node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "")
end
node_stack.pop
else
open_start = node_stack.last.position - 20
open_start = 0 if open_start < 0
close_start = node.position - 20
close_start = 0 if close_start < 0
msg = <<EOF.strip
ignoring attempt to close #{node_stack.last.name} with #{node.name}
opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
closed at byte #{node.position}, line #{node.line}
attributes at open: #{node_stack.last.attributes.inspect}
text around open: #{text[open_start,40].inspect}
text around close: #{text[close_start,40].inspect}
EOF
strict ? raise(msg) : warn(msg)
end
elsif !node.childless?(xml) && node.closing != :close
node_stack.push node
end
end
end
end
# Search the tree for (and return) the first node that matches the given
# conditions. The conditions are interpreted differently for different node
# types, see HTML::Text#find and HTML::Tag#find.
def find(conditions)
@root.find(conditions)
end
# Search the tree for (and return) all nodes that match the given
# conditions. The conditions are interpreted differently for different node
# types, see HTML::Text#find and HTML::Tag#find.
def find_all(conditions)
@root.find_all(conditions)
end
end
end
require 'strscan'
module HTML #:nodoc:
class Conditions < Hash #:nodoc:
def initialize(hash)
super()
hash = { :content => hash } unless Hash === hash
hash = keys_to_symbols(hash)
hash.each do |k,v|
case k
when :tag, :content then
# keys are valid, and require no further processing
when :attributes then
hash[k] = keys_to_strings(v)
when :parent, :child, :ancestor, :descendant, :sibling, :before,
:after
hash[k] = Conditions.new(v)
when :children
hash[k] = v = keys_to_symbols(v)
v.each do |k,v2|
case k
when :count, :greater_than, :less_than
# keys are valid, and require no further processing
when :only
v[k] = Conditions.new(v2)
else
raise "illegal key #{k.inspect} => #{v2.inspect}"
end
end
else
raise "illegal key #{k.inspect} => #{v.inspect}"
end
end
update hash
end
private
def keys_to_strings(hash)
hash.keys.inject({}) do |h,k|
h[k.to_s] = hash[k]
h
end
end
def keys_to_symbols(hash)
hash.keys.inject({}) do |h,k|
raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
h[k.to_sym] = hash[k]
h
end
end
end
# The base class of all nodes, textual and otherwise, in an HTML document.
class Node #:nodoc:
# The array of children of this node. Not all nodes have children.
attr_reader :children
# The parent node of this node. All nodes have a parent, except for the
# root node.
attr_reader :parent
# The line number of the input where this node was begun
attr_reader :line
# The byte position in the input where this node was begun
attr_reader :position
# Create a new node as a child of the given parent.
def initialize(parent, line=0, pos=0)
@parent = parent
@children = []
@line, @position = line, pos
end
# Return a textual representation of the node.
def to_s
s = ""
@children.each { |child| s << child.to_s }
s
end
# Return false (subclasses must override this to provide specific matching
# behavior.) +conditions+ may be of any type.
def match(conditions)
false
end
# Search the children of this node for the first node for which #find
# returns non +nil+. Returns the result of the #find call that succeeded.
def find(conditions)
conditions = validate_conditions(conditions)
@children.each do |child|
node = child.find(conditions)
return node if node
end
nil
end
# Search for all nodes that match the given conditions, and return them
# as an array.
def find_all(conditions)
conditions = validate_conditions(conditions)
matches = []
matches << self if match(conditions)
@children.each do |child|
matches.concat child.find_all(conditions)
end
matches
end
# Returns +false+. Subclasses may override this if they define a kind of
# tag.
def tag?
false
end
def validate_conditions(conditions)
Conditions === conditions ? conditions : Conditions.new(conditions)
end
def ==(node)
return false unless self.class == node.class && children.size == node.children.size
equivalent = true
children.size.times do |i|
equivalent &&= children[i] == node.children[i]
end
equivalent
end
class <<self
def parse(parent, line, pos, content, strict=true)
if content !~ /^<\S/
Text.new(parent, line, pos, content)
else
scanner = StringScanner.new(content)
unless scanner.skip(/</)
if strict
raise "expected <"
else
return Text.new(parent, line, pos, content)
end
end
if scanner.skip(/!\[CDATA\[/)
unless scanner.skip_until(/\]\]>/)
if strict
raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
else
scanner.skip_until(/\Z/)
end
end
return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
end
closing = ( scanner.scan(/\//) ? :close : nil )
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/)
name.downcase!
unless closing
scanner.skip(/\s*/)
attributes = {}
while attr = scanner.scan(/[-\w:]+/)
value = true
if scanner.scan(/\s*=\s*/)
if delim = scanner.scan(/['"]/)
value = ""
while text = scanner.scan(/[^#{delim}\\]+|./)
case text
when "\\" then
value << text
value << scanner.getch
when delim
break
else value << text
end
end
else
value = scanner.scan(/[^\s>\/]+/)
end
end
attributes[attr.downcase] = value
scanner.skip(/\s*/)
end
closing = ( scanner.scan(/\//) ? :self : nil )
end
unless scanner.scan(/\s*>/)
if strict
raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
else
# throw away all text until we find what we're looking for
scanner.skip_until(/>/) or scanner.terminate
end
end
Tag.new(parent, line, pos, name, attributes, closing)
end
end
end
end
# A node that represents text, rather than markup.
class Text < Node #:nodoc:
attr_reader :content
# Creates a new text node as a child of the given parent, with the given
# content.
def initialize(parent, line, pos, content)
super(parent, line, pos)
@content = content
end
# Returns the content of this node.
def to_s
@content
end
# Returns +self+ if this node meets the given conditions. Text nodes support
# conditions of the following kinds:
#
# * if +conditions+ is a string, it must be a substring of the node's
# content
# * if +conditions+ is a regular expression, it must match the node's
# content
# * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
# is either a string or a regexp, and which is interpreted as described
# above.
def find(conditions)
match(conditions) && self
end
# Returns non-+nil+ if this node meets the given conditions, or +nil+
# otherwise. See the discussion of #find for the valid conditions.
def match(conditions)
case conditions
when String
@content == conditions
when Regexp
@content =~ conditions
when Hash
conditions = validate_conditions(conditions)
# Text nodes only have :content, :parent, :ancestor
unless (conditions.keys - [:content, :parent, :ancestor]).empty?
return false
end
match(conditions[:content])
else
nil
end
end
def ==(node)
return false unless super
content == node.content
end
end
# A CDATA node is simply a text node with a specialized way of displaying
# itself.
class CDATA < Text #:nodoc:
def to_s
"<![CDATA[#{super}]]>"
end
end
# A Tag is any node that represents markup. It may be an opening tag, a
# closing tag, or a self-closing tag. It has a name, and may have a hash of
# attributes.
class Tag < Node #:nodoc:
# Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
attr_reader :closing
# Either +nil+, or a hash of attributes for this node.
attr_reader :attributes
# The name of this tag.
attr_reader :name
# Create a new node as a child of the given parent, using the given content
# to describe the node. It will be parsed and the node name, attributes and
# closing status extracted.
def initialize(parent, line, pos, name, attributes, closing)
super(parent, line, pos)
@name = name
@attributes = attributes
@closing = closing
end
# A convenience for obtaining an attribute of the node. Returns +nil+ if
# the node has no attributes.
def [](attr)
@attributes ? @attributes[attr] : nil
end
# Returns non-+nil+ if this tag can contain child nodes.
def childless?(xml = false)
return false if xml && @closing.nil?
[email protected]? ||
@name =~ /^(img|br|hr|link|meta|area|base|basefont|
col|frame|input|isindex|param)$/ox
end
# Returns a textual representation of the node
def to_s
if @closing == :close
"</#{@name}>"
else
s = "<#{@name}"
@attributes.each do |k,v|
s << " #{k}"
s << "=\"#{v}\"" if String === v
end
s << " /" if @closing == :self
s << ">"
@children.each { |child| s << child.to_s }
s << "</#{@name}>" if @closing != :self && [email protected]?
s
end
end
# If either the node or any of its children meet the given conditions, the
# matching node is returned. Otherwise, +nil+ is returned. (See the
# description of the valid conditions in the +match+ method.)
def find(conditions)
match(conditions) && self || super
end
# Returns +true+, indicating that this node represents an HTML tag.
def tag?
true
end
# Returns +true+ if the node meets any of the given conditions. The
# +conditions+ parameter must be a hash of any of the following keys
# (all are optional):
#
# * <tt>:tag</tt>: the node name must match the corresponding value
# * <tt>:attributes</tt>: a hash. The node's values must match the
# corresponding values in the hash.
# * <tt>:parent</tt>: a hash. The node's parent must match the
# corresponding hash.
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
# must meet the criteria described by the hash.
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
# meet the criteria described by the hash.
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
# must meet the criteria described by the hash.
# * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
# meet the criteria described by the hash.
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
# keys:
# ** <tt>:count</tt>: either a number or a range which must equal (or
# include) the number of children that match.
# ** <tt>:less_than</tt>: the number of matching children must be less than
# this number.
# ** <tt>:greater_than</tt>: the number of matching children must be
# greater than this number.
# ** <tt>:only</tt>: another hash consisting of the keys to use
# to match on the children, and only matching children will be
# counted.
#
# Conditions are matched using the following algorithm:
#
# * if the condition is a string, it must be a substring of the value.
# * if the condition is a regexp, it must match the value.
# * if the condition is a number, the value must match number.to_s.
# * if the condition is +true+, the value must not be +nil+.
# * if the condition is +false+ or +nil+, the value must be +nil+.
#
# Usage:
#
# # test if the node is a "span" tag
# node.match :tag => "span"
#
# # test if the node's parent is a "div"
# node.match :parent => { :tag => "div" }
#
# # test if any of the node's ancestors are "table" tags
# node.match :ancestor => { :tag => "table" }
#
# # test if any of the node's immediate children are "em" tags
# node.match :child => { :tag => "em" }
#
# # test if any of the node's descendants are "strong" tags
# node.match :descendant => { :tag => "strong" }
#
# # test if the node has between 2 and 4 span tags as immediate children
# node.match :children => { :count => 2..4, :only => { :tag => "span" } }
#
# # get funky: test to see if the node is a "div", has a "ul" ancestor
# # and an "li" parent (with "class" = "enum"), and whether or not it has
# # a "span" descendant that contains # text matching /hello world/:
# node.match :tag => "div",
# :ancestor => { :tag => "ul" },
# :parent => { :tag => "li",
# :attributes => { :class => "enum" } },
# :descendant => { :tag => "span",
# :child => /hello world/ }
def match(conditions)
conditions = validate_conditions(conditions)
# check content of child nodes
if conditions[:content]
if children.empty?
return false unless match_condition("", conditions[:content])
else
return false unless children.find { |child| child.match(conditions[:content]) }
end
end
# test the name
return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
# test attributes
(conditions[:attributes] || {}).each do |key, value|
return false unless match_condition(self[key], value)
end
# test parent
return false unless parent.match(conditions[:parent]) if conditions[:parent]
# test children
return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
# test ancestors
if conditions[:ancestor]
return false unless catch :found do
p = self
throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
end
end
# test descendants
if conditions[:descendant]
return false unless children.find do |child|
# test the child
child.match(conditions[:descendant]) ||
# test the child's descendants
child.match(:descendant => conditions[:descendant])
end
end
# count children
if opts = conditions[:children]
matches = children.select do |c|
(c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
end
matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
opts.each do |key, value|
next if key == :only
case key
when :count
if Integer === value
return false if matches.length != value
else
return false unless value.include?(matches.length)
end
when :less_than
return false unless matches.length < value
when :greater_than
return false unless matches.length > value
else raise "unknown count condition #{key}"
end
end
end
# test siblings
if conditions[:sibling] || conditions[:before] || conditions[:after]
siblings = parent ? parent.children : []
self_index = siblings.index(self)
if conditions[:sibling]
return false unless siblings.detect do |s|
s != self && s.match(conditions[:sibling])
end
end
if conditions[:before]
return false unless siblings[self_index+1..-1].detect do |s|
s != self && s.match(conditions[:before])
end
end
if conditions[:after]
return false unless siblings[0,self_index].detect do |s|
s != self && s.match(conditions[:after])
end
end
end
true
end
def ==(node)
return false unless super
return false unless closing == node.closing && self.name == node.name
attributes == node.attributes
end
private
# Match the given value to the given condition.
def match_condition(value, condition)
case condition
when String
value && value == condition
when Regexp
value && value.match(condition)
when Numeric
value == condition.to_s
when true
!value.nil?
when false, nil
value.nil?
else
false
end
end
end
end
require 'set'
require 'active_support/core_ext/class/inheritable_attributes'
module HTML
class Sanitizer
def sanitize(text, options = {})
return text unless sanitizeable?(text)
tokenize(text, options).join
end
def sanitizeable?(text)
!(text.nil? || text.empty? || !text.index("<"))
end
protected
def tokenize(text, options)
tokenizer = HTML::Tokenizer.new(text)
result = []
while token = tokenizer.next
node = Node.parse(nil, 0, 0, token, false)
process_node node, result, options
end
result
end
def process_node(node, result, options)
result << node.to_s
end
end
class FullSanitizer < Sanitizer
def sanitize(text, options = {})
result = super
# strip any comments, and if they have a newline at the end (ie. line with
# only a comment) strip that too
result.gsub!(/<!--(.*?)-->[\n]?/m, "") if result
# Recurse - handle all dirty nested tags
result == text ? result : sanitize(result, options)
end
def process_node(node, result, options)
result << node.to_s if node.class == HTML::Text
end
end
class LinkSanitizer < FullSanitizer
cattr_accessor :included_tags, :instance_writer => false
self.included_tags = Set.new(%w(a href))
def sanitizeable?(text)
!(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
end
protected
def process_node(node, result, options)
result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
end
end
class WhiteListSanitizer < Sanitizer
[:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
:allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
class_inheritable_accessor attr, :instance_writer => false
end
# A regular expression of the valid characters used to separate protocols like
# the ':' in 'http://foo.com'
self.protocol_separator = /:|(&#0*58)|(&#x70)|(%|&#37;)3A/
# Specifies a Set of HTML attributes that can have URIs.
self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
# Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
# to just escaping harmless tags like &lt;font&gt;
self.bad_tags = Set.new(%w(script))
# Specifies the default Set of tags that the #sanitize helper will allow unscathed.
self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
acronym a img blockquote del ins))
# Specifies the default Set of html attributes that the #sanitize helper will leave
# in the allowed tag.
self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
# Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
feed svn urn aim rsync tag ssh sftp rtsp afs))
# Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
border-color border-left-color border-right-color border-top-color clear color cursor direction display
elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
width))
# Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
nowrap olive pointer purple red right solid silver teal top transparent underline white yellow))
# Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
self.shorthand_css_properties = Set.new(%w(background border margin padding))
# Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
def sanitize_css(style)
# disallow urls
style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
# gauntlet
if style !~ /^([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*$/ ||
style !~ /^(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*$/
return ''
end
clean = []
style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
if allowed_css_properties.include?(prop.downcase)
clean << prop + ': ' + val + ';'
elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
unless val.split().any? do |keyword|
!allowed_css_keywords.include?(keyword) &&
keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/
end
clean << prop + ': ' + val + ';'
end
end
end
clean.join(' ')
end
protected
def tokenize(text, options)
options[:parent] = []
options[:attributes] ||= allowed_attributes
options[:tags] ||= allowed_tags
super
end
def process_node(node, result, options)
result << case node
when HTML::Tag
if node.closing == :close
options[:parent].shift
else
options[:parent].unshift node.name
end
process_attributes_for node, options
options[:tags].include?(node.name) ? node : nil
else
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
end
end
def process_attributes_for(node, options)
return unless node.attributes
node.attributes.keys.each do |attr_name|
value = node.attributes[attr_name].to_s
if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value)
node.attributes.delete(attr_name)
else
node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value))
end
end
end
def contains_bad_protocols?(attr_name, value)
uri_attributes.include?(attr_name) &&
(value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(%|&#37;)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first))
end
end
end
#--
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
# Under MIT and/or CC By license.
#++
module HTML
# Selects HTML elements using CSS 2 selectors.
#
# The +Selector+ class uses CSS selector expressions to match and select
# HTML elements.
#
# For example:
# selector = HTML::Selector.new "form.login[action=/login]"
# creates a new selector that matches any +form+ element with the class
# +login+ and an attribute +action+ with the value <tt>/login</tt>.
#
# === Matching Elements
#
# Use the #match method to determine if an element matches the selector.
#
# For simple selectors, the method returns an array with that element,
# or +nil+ if the element does not match. For complex selectors (see below)
# the method returns an array with all matched elements, of +nil+ if no
# match found.
#
# For example:
# if selector.match(element)
# puts "Element is a login form"
# end
#
# === Selecting Elements
#
# Use the #select method to select all matching elements starting with
# one element and going through all children in depth-first order.
#
# This method returns an array of all matching elements, an empty array
# if no match is found
#
# For example:
# selector = HTML::Selector.new "input[type=text]"
# matches = selector.select(element)
# matches.each do |match|
# puts "Found text field with name #{match.attributes['name']}"
# end
#
# === Expressions
#
# Selectors can match elements using any of the following criteria:
# * <tt>name</tt> -- Match an element based on its name (tag name).
# For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt>
# to match any element.
# * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the
# <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>.
# * <tt>.class</tt> -- Match an element based on its class name, all
# class names if more than one specified.
# * <tt>[attr]</tt> -- Match an element that has the specified attribute.
# * <tt>[attr=value]</tt> -- Match an element that has the specified
# attribute and value. (More operators are supported see below)
# * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class,
# such as <tt>:nth-child</tt> and <tt>:empty</tt>.
# * <tt>:not(expr)</tt> -- Match an element that does not match the
# negation expression.
#
# When using a combination of the above, the element name comes first
# followed by identifier, class names, attributes, pseudo classes and
# negation in any order. Do not separate these parts with spaces!
# Space separation is used for descendant selectors.
#
# For example:
# selector = HTML::Selector.new "form.login[action=/login]"
# The matched element must be of type +form+ and have the class +login+.
# It may have other classes, but the class +login+ is required to match.
# It must also have an attribute called +action+ with the value
# <tt>/login</tt>.
#
# This selector will match the following element:
# <form class="login form" method="post" action="/login">
# but will not match the element:
# <form method="post" action="/logout">
#
# === Attribute Values
#
# Several operators are supported for matching attributes:
# * <tt>name</tt> -- The element must have an attribute with that name.
# * <tt>name=value</tt> -- The element must have an attribute with that
# name and value.
# * <tt>name^=value</tt> -- The attribute value must start with the
# specified value.
# * <tt>name$=value</tt> -- The attribute value must end with the
# specified value.
# * <tt>name*=value</tt> -- The attribute value must contain the
# specified value.
# * <tt>name~=word</tt> -- The attribute value must contain the specified
# word (space separated).
# * <tt>name|=word</tt> -- The attribute value must start with specified
# word.
#
# For example, the following two selectors match the same element:
# #my_id
# [id=my_id]
# and so do the following two selectors:
# .my_class
# [class~=my_class]
#
# === Alternatives, siblings, children
#
# Complex selectors use a combination of expressions to match elements:
# * <tt>expr1 expr2</tt> -- Match any element against the second expression
# if it has some parent element that matches the first expression.
# * <tt>expr1 > expr2</tt> -- Match any element against the second expression
# if it is the child of an element that matches the first expression.
# * <tt>expr1 + expr2</tt> -- Match any element against the second expression
# if it immediately follows an element that matches the first expression.
# * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression
# that comes after an element that matches the first expression.
# * <tt>expr1, expr2</tt> -- Match any element against the first expression,
# or against the second expression.
#
# Since children and sibling selectors may match more than one element given
# the first element, the #match method may return more than one match.
#
# === Pseudo classes
#
# Pseudo classes were introduced in CSS 3. They are most often used to select
# elements in a given position:
# * <tt>:root</tt> -- Match the element only if it is the root element
# (no parent element).
# * <tt>:empty</tt> -- Match the element only if it has no child elements,
# and no text content.
# * <tt>:only-child</tt> -- Match the element if it is the only child (element)
# of its parent element.
# * <tt>:only-of-type</tt> -- Match the element if it is the only child (element)
# of its parent element and its type.
# * <tt>:first-child</tt> -- Match the element if it is the first child (element)
# of its parent element.
# * <tt>:first-of-type</tt> -- Match the element if it is the first child (element)
# of its parent element of its type.
# * <tt>:last-child</tt> -- Match the element if it is the last child (element)
# of its parent element.
# * <tt>:last-of-type</tt> -- Match the element if it is the last child (element)
# of its parent element of its type.
# * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element)
# of its parent element. The value <tt>b</tt> specifies its index, starting with 1.
# * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element)
# in each group of <tt>a</tt> child elements of its parent element.
# * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element)
# in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child
# elements of its parent element.
# * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third).
# Same as <tt>:nth-child(2n+1)</tt>.
# * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second,
# fourth). Same as <tt>:nth-child(2n+2)</tt>.
# * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type.
# * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child.
# * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and
# only elements of its type.
# * <tt>:not(selector)</tt> -- Match the element only if the element does not
# match the simple selector.
#
# As you can see, <tt>:nth-child<tt> pseudo class and its variant can get quite
# tricky and the CSS specification doesn't do a much better job explaining it.
# But after reading the examples and trying a few combinations, it's easy to
# figure out.
#
# For example:
# table tr:nth-child(odd)
# Selects every second row in the table starting with the first one.
#
# div p:nth-child(4)
# Selects the fourth paragraph in the +div+, but not if the +div+ contains
# other elements, since those are also counted.
#
# div p:nth-of-type(4)
# Selects the fourth paragraph in the +div+, counting only paragraphs, and
# ignoring all other elements.
#
# div p:nth-of-type(-n+4)
# Selects the first four paragraphs, ignoring all others.
#
# And you can always select an element that matches one set of rules but
# not another using <tt>:not</tt>. For example:
# p:not(.post)
# Matches all paragraphs that do not have the class <tt>.post</tt>.
#
# === Substitution Values
#
# You can use substitution with identifiers, class names and element values.
# A substitution takes the form of a question mark (<tt>?</tt>) and uses the
# next value in the argument list following the CSS expression.
#
# The substitution value may be a string or a regular expression. All other
# values are converted to strings.
#
# For example:
# selector = HTML::Selector.new "#?", /^\d+$/
# matches any element whose identifier consists of one or more digits.
#
# See http://www.w3.org/TR/css3-selectors/
class Selector
# An invalid selector.
class InvalidSelectorError < StandardError #:nodoc:
end
class << self
# :call-seq:
# Selector.for_class(cls) => selector
#
# Creates a new selector for the given class name.
def for_class(cls)
self.new([".?", cls])
end
# :call-seq:
# Selector.for_id(id) => selector
#
# Creates a new selector for the given id.
def for_id(id)
self.new(["#?", id])
end
end
# :call-seq:
# Selector.new(string, [values ...]) => selector
#
# Creates a new selector from a CSS 2 selector expression.
#
# The first argument is the selector expression. All other arguments
# are used for value substitution.
#
# Throws InvalidSelectorError is the selector expression is invalid.
def initialize(selector, *values)
raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
@source = ""
values = values[0] if values.size == 1 && values[0].is_a?(Array)
# We need a copy to determine if we failed to parse, and also
# preserve the original pass by-ref statement.
statement = selector.strip.dup
# Create a simple selector, along with negation.
simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
@alternates = []
@depends = nil
# Alternative selector.
if statement.sub!(/^\s*,\s*/, "")
second = Selector.new(statement, values)
@alternates << second
# If there are alternate selectors, we group them in the top selector.
if alternates = second.instance_variable_get(:@alternates)
second.instance_variable_set(:@alternates, [])
@alternates.concat alternates
end
@source << " , " << second.to_s
# Sibling selector: create a dependency into second selector that will
# match element immediately following this one.
elsif statement.sub!(/^\s*\+\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
if element = next_element(element)
second.match(element, first)
end
end
@source << " + " << second.to_s
# Adjacent selector: create a dependency into second selector that will
# match all elements following this one.
elsif statement.sub!(/^\s*~\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
while element = next_element(element)
if subset = second.match(element, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
end
end
matches.empty? ? nil : matches
end
@source << " ~ " << second.to_s
# Child selector: create a dependency into second selector that will
# match a child element of this one.
elsif statement.sub!(/^\s*>\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
element.children.each do |child|
if child.tag? && subset = second.match(child, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
end
end
matches.empty? ? nil : matches
end
@source << " > " << second.to_s
# Descendant selector: create a dependency into second selector that
# will match all descendant elements of this one. Note,
elsif statement =~ /^\s+\S+/ && statement != selector
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
stack = element.children.reverse
while node = stack.pop
next unless node.tag?
if subset = second.match(node, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
elsif children = node.children
stack.concat children.reverse
end
end
matches.empty? ? nil : matches
end
@source << " " << second.to_s
else
# The last selector is where we check that we parsed
# all the parts.
unless statement.empty? || statement.strip.empty?
raise ArgumentError, "Invalid selector: #{statement}"
end
end
end
# :call-seq:
# match(element, first?) => array or nil
#
# Matches an element against the selector.
#
# For a simple selector this method returns an array with the
# element if the element matches, nil otherwise.
#
# For a complex selector (sibling and descendant) this method
# returns an array with all matching elements, nil if no match is
# found.
#
# Use +first_only=true+ if you are only interested in the first element.
#
# For example:
# if selector.match(element)
# puts "Element is a login form"
# end
def match(element, first_only = false)
# Match element if no element name or element name same as element name
if matched = (!@tag_name || @tag_name == element.name)
# No match if one of the attribute matches failed
for attr in @attributes
if element.attributes[attr[0]] !~ attr[1]
matched = false
break
end
end
end
# Pseudo class matches (nth-child, empty, etc).
if matched
for pseudo in @pseudo
unless pseudo.call(element)
matched = false
break
end
end
end
# Negation. Same rules as above, but we fail if a match is made.
if matched && @negation
for negation in @negation
if negation[:tag_name] == element.name
matched = false
else
for attr in negation[:attributes]
if element.attributes[attr[0]] =~ attr[1]
matched = false
break
end
end
end
if matched
for pseudo in negation[:pseudo]
if pseudo.call(element)
matched = false
break
end
end
end
break unless matched
end
end
# If element matched but depends on another element (child,
# sibling, etc), apply the dependent matches instead.
if matched && @depends
matches = @depends.call(element, first_only)
else
matches = matched ? [element] : nil
end
# If this selector is part of the group, try all the alternative
# selectors (unless first_only).
if !first_only || !matches
@alternates.each do |alternate|
break if matches && first_only
if subset = alternate.match(element, first_only)
if matches
matches.concat subset
else
matches = subset
end
end
end
end
matches
end
# :call-seq:
# select(root) => array
#
# Selects and returns an array with all matching elements, beginning
# with one node and traversing through all children depth-first.
# Returns an empty array if no match is found.
#
# The root node may be any element in the document, or the document
# itself.
#
# For example:
# selector = HTML::Selector.new "input[type=text]"
# matches = selector.select(element)
# matches.each do |match|
# puts "Found text field with name #{match.attributes['name']}"
# end
def select(root)
matches = []
stack = [root]
while node = stack.pop
if node.tag? && subset = match(node, false)
subset.each do |match|
matches << match unless matches.any? { |item| item.equal?(match) }
end
elsif children = node.children
stack.concat children.reverse
end
end
matches
end
# Similar to #select but returns the first matching element. Returns +nil+
# if no element matches the selector.
def select_first(root)
stack = [root]
while node = stack.pop
if node.tag? && subset = match(node, true)
return subset.first if !subset.empty?
elsif children = node.children
stack.concat children.reverse
end
end
nil
end
def to_s #:nodoc:
@source
end
# Return the next element after this one. Skips sibling text nodes.
#
# With the +name+ argument, returns the next element with that name,
# skipping other sibling elements.
def next_element(element, name = nil)
if siblings = element.parent.children
found = false
siblings.each do |node|
if node.equal?(element)
found = true
elsif found && node.tag?
return node if (name.nil? || node.name == name)
end
end
end
nil
end
protected
# Creates a simple selector given the statement and array of
# substitution values.
#
# Returns a hash with the values +tag_name+, +attributes+,
# +pseudo+ (classes) and +negation+.
#
# Called the first time with +can_negate+ true to allow
# negation. Called a second time with false since negation
# cannot be negated.
def simple_selector(statement, values, can_negate = true)
tag_name = nil
attributes = []
pseudo = []
negation = []
# Element name. (Note that in negation, this can come at
# any order, but for simplicity we allow if only first).
statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
match.strip!
tag_name = match.downcase unless match == "*"
@source << match
"" # Remove
end
# Get identifier, class, attribute name, pseudo or negation.
while true
# Element identifier.
next if statement.sub!(/^#(\?|[\w\-]+)/) do |match|
id = $1
if id == "?"
id = values.shift
end
@source << "##{id}"
id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
attributes << ["id", id]
"" # Remove
end
# Class name.
next if statement.sub!(/^\.([\w\-]+)/) do |match|
class_name = $1
@source << ".#{class_name}"
class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
attributes << ["class", class_name]
"" # Remove
end
# Attribute value.
next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
name, equality, value = $1, $2, $3
if value == "?"
value = values.shift
else
# Handle single and double quotes.
value.strip!
if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
value = value[1..-2]
end
end
@source << "[#{name}#{equality}'#{value}']"
attributes << [name.downcase.strip, attribute_match(equality, value)]
"" # Remove
end
# Root element only.
next if statement.sub!(/^:root/) do |match|
pseudo << lambda do |element|
element.parent.nil? || !element.parent.tag?
end
@source << ":root"
"" # Remove
end
# Nth-child including last and of-type.
next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
reverse = $1 == "last-"
of_type = $2 == "of-type"
@source << ":nth-#{$1}#{$2}("
case $3
when "odd"
pseudo << nth_child(2, 1, of_type, reverse)
@source << "odd)"
when "even"
pseudo << nth_child(2, 2, of_type, reverse)
@source << "even)"
when /^(\d+|\?)$/ # b only
b = ($1 == "?" ? values.shift : $1).to_i
pseudo << nth_child(0, b, of_type, reverse)
@source << "#{b})"
when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
a = ($1 == "?" ? values.shift :
$1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
b = ($2 == "?" ? values.shift : $2).to_i
pseudo << nth_child(a, b, of_type, reverse)
@source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
else
raise ArgumentError, "Invalid nth-child #{match}"
end
"" # Remove
end
# First/last child (of type).
next if statement.sub!(/^:(first|last)-(child|of-type)/) do |match|
reverse = $1 == "last"
of_type = $2 == "of-type"
pseudo << nth_child(0, 1, of_type, reverse)
@source << ":#{$1}-#{$2}"
"" # Remove
end
# Only child (of type).
next if statement.sub!(/^:only-(child|of-type)/) do |match|
of_type = $1 == "of-type"
pseudo << only_child(of_type)
@source << ":only-#{$1}"
"" # Remove
end
# Empty: no child elements or meaningful content (whitespaces
# are ignored).
next if statement.sub!(/^:empty/) do |match|
pseudo << lambda do |element|
empty = true
for child in element.children
if child.tag? || !child.content.strip.empty?
empty = false
break
end
end
empty
end
@source << ":empty"
"" # Remove
end
# Content: match the text content of the element, stripping
# leading and trailing spaces.
next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do |match|
content = $1
if content == "?"
content = values.shift
elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
content = content[1..-2]
end
@source << ":content('#{content}')"
content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
pseudo << lambda do |element|
text = ""
for child in element.children
unless child.tag?
text << child.content
end
end
text.strip =~ content
end
"" # Remove
end
# Negation. Create another simple selector to handle it.
if statement.sub!(/^:not\(\s*/, "")
raise ArgumentError, "Double negatives are not missing feature" unless can_negate
@source << ":not("
negation << simple_selector(statement, values, false)
raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
@source << ")"
next
end
# No match: moving on.
break
end
# Return hash. The keys are mapped to instance variables.
{:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
end
# Create a regular expression to match an attribute value based
# on the equality operator (=, ^=, |=, etc).
def attribute_match(equality, value)
regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
case equality
when "=" then
# Match the attribute value in full
Regexp.new("^#{regexp}$")
when "~=" then
# Match a space-separated word within the attribute value
Regexp.new("(^|\s)#{regexp}($|\s)")
when "^="
# Match the beginning of the attribute value
Regexp.new("^#{regexp}")
when "$="
# Match the end of the attribute value
Regexp.new("#{regexp}$")
when "*="
# Match substring of the attribute value
regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
when "|=" then
# Match the first space-separated item of the attribute value
Regexp.new("^#{regexp}($|\s)")
else
raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
# Match all attributes values (existence check)
//
end
end
# Returns a lambda that can match an element against the nth-child
# pseudo class, given the following arguments:
# * +a+ -- Value of a part.
# * +b+ -- Value of b part.
# * +of_type+ -- True to test only elements of this type (of-type).
# * +reverse+ -- True to count in reverse order (last-).
def nth_child(a, b, of_type, reverse)
# a = 0 means select at index b, if b = 0 nothing selected
return lambda { |element| false } if a == 0 && b == 0
# a < 0 and b < 0 will never match against an index
return lambda { |element| false } if a < 0 && b < 0
b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
lambda do |element|
# Element must be inside parent element.
return false unless element.parent && element.parent.tag?
index = 0
# Get siblings, reverse if counting from last.
siblings = element.parent.children
siblings = siblings.reverse if reverse
# Match element name if of-type, otherwise ignore name.
name = of_type ? element.name : nil
found = false
for child in siblings
# Skip text nodes/comments.
if child.tag? && (name == nil || child.name == name)
if a == 0
# Shortcut when a == 0 no need to go past count
if index == b
found = child.equal?(element)
break
end
elsif a < 0
# Only look for first b elements
break if index > b
if child.equal?(element)
found = (index % a) == 0
break
end
else
# Otherwise, break if child found and count == an+b
if child.equal?(element)
found = (index % a) == b
break
end
end
index += 1
end
end
found
end
end
# Creates a only child lambda. Pass +of-type+ to only look at
# elements of its type.
def only_child(of_type)
lambda do |element|
# Element must be inside parent element.
return false unless element.parent && element.parent.tag?
name = of_type ? element.name : nil
other = false
for child in element.parent.children
# Skip text nodes/comments.
if child.tag? && (name == nil || child.name == name)
unless child.equal?(element)
other = true
break
end
end
end
!other
end
end
# Called to create a dependent selector (sibling, descendant, etc).
# Passes the remainder of the statement that will be reduced to zero
# eventually, and array of substitution values.
#
# This method is called from four places, so it helps to put it here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment