Created
January 15, 2010 03:16
-
-
Save dsturnbull/277776 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
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' | |
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 | |
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 | |
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 | |
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 | |
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=" ") | |
# 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 = /:|(�*58)|(p)|(%|%)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 <font> | |
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(/</, "<") | |
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 =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment