Merge remote branch 'mainstream/master'

This commit is contained in:
Pratik Naik 2010-01-04 03:24:39 +05:30
commit cda36a0731
555 changed files with 13722 additions and 19244 deletions

8
.gitignore vendored

@ -7,16 +7,9 @@ activerecord/doc
actionpack/doc
actionmailer/doc
activesupport/doc
activemodel/pkg
activeresource/pkg
activerecord/pkg
actionpack/pkg
activemodel/test/fixtures/fixture_database.sqlite3
actionmailer/pkg
activesupport/pkg
actionpack/test/tmp
activesupport/test/fixtures/isolation_test
railties/pkg
railties/test/500.html
railties/test/fixtures/tmp
railties/test/initializer/root/log
@ -31,3 +24,4 @@ railties/guides/output
bin
vendor/gems/
railties/tmp
pkg

6
.gitmodules vendored

@ -1,6 +1,6 @@
[submodule "arel"]
path = arel
url = git://github.com/rails/arel.git
[submodule "rack-mount"]
path = rack-mount
url = git://github.com/rails/rack-mount.git
[submodule "rack"]
path = rack
url = git://github.com/rails/rack.git

24
Gemfile

@ -1,23 +1,27 @@
clear_sources
source 'http://gemcutter.org'
gem "rake", ">= 0.8.7"
gem "mocha", ">= 0.9.8"
gem "ruby-debug", ">= 0.10.3" if RUBY_VERSION < '1.9'
gem "rails", "3.0.pre", :vendored_at => "railties"
gem "rails", "3.0.pre", :path => "railties"
%w(activesupport activemodel actionpack actionmailer activerecord activeresource).each do |lib|
gem lib, '3.0.pre', :vendored_at => lib
gem lib, '3.0.pre', :path => lib
end
# AS
gem "i18n", ">= 0.3.0"
# AR
gem "arel", "0.2.pre", :git => "git://github.com/rails/arel.git"
gem "sqlite3-ruby", ">= 1.2.5"
gem "pg", ">= 0.8.0"
gem "mysql", ">= 2.8.1"
only :test do
gem "pg", ">= 0.8.0"
gem "mysql", ">= 2.8.1"
end
# AP
gem "rack", "1.0.1", :git => "git://github.com/rails/rack.git"
gem "rack-mount", :git => "git://github.com/rails/rack-mount.git"
gem "rack", "1.1.0", :git => "git://github.com/rack/rack.git"
gem "rack-test", "0.5.3"
gem "RedCloth", ">= 4.2.2"
if ENV['CI']
@ -34,3 +38,5 @@ if ENV['CI']
gem "test-unit", ">= 2.0.5"
end
end
disable_system_gems

@ -1,9 +1,10 @@
require 'rake'
require 'rake/rdoctask'
require 'rake/gempackagetask'
env = %(PKG_BUILD="#{ENV['PKG_BUILD']}") if ENV['PKG_BUILD']
PROJECTS = %w(activesupport actionpack actionmailer activeresource activerecord activemodel railties)
PROJECTS = %w(activesupport activemodel actionpack actionmailer activeresource activerecord railties)
Dir["#{File.dirname(__FILE__)}/*/lib/*/version.rb"].each do |version_path|
require version_path
@ -23,11 +24,28 @@ task :default => %w(test test:isolated)
end
end
desc "Smoke-test all projects"
task :smoke do
(PROJECTS - %w(activerecord)).each do |project|
system %(cd #{project} && #{env} #{$0} test:isolated)
end
system %(cd activerecord && #{env} #{$0} sqlite3:isolated_test)
end
spec = eval(File.read('rails.gemspec'))
Rake::GemPackageTask.new(spec) do |pkg|
pkg.gem_spec = spec
end
task :install => :gem do
system %(cd arel && gem build arel.gemspec && gem install arel-0.2.pre.gem --no-ri --no-rdoc --ignore-dependencies)
system %(cd rack && rake gem VERSION=1.0.2.pre && gem install rack-1.0.2.pre.gem --no-ri --no-rdoc --ignore-dependencies)
(PROJECTS - ["railties"]).each do |project|
puts "INSTALLING #{project}"
system("gem install #{project}/pkg/#{project}-#{ActionPack::VERSION::STRING}.gem --no-ri --no-rdoc")
end
system("gem install railties/pkg/rails-#{ActionPack::VERSION::STRING}.gem --no-ri --no-rdoc")
system("gem install railties/pkg/railties-#{ActionPack::VERSION::STRING}.gem --no-ri --no-rdoc")
system("gem install pkg/rails-#{ActionPack::VERSION::STRING}.gem --no-ri --no-rdoc")
end
desc "Generate documentation for the Rails framework"

@ -1,3 +1,27 @@
*Rails 3.0 (pending)*
* The Mail::Message class has helped methods for all the field types that return 'common' defaults for the common use case, so to get the subject, mail.subject will give you a string, mail.date will give you a DateTime object, mail.from will give you an array of address specs (mikel@test.lindsaar.net) etc. If you want to access the field object itself, call mail[:field_name] which will return the field object you want, which you can then chain, like mail[:from].formatted
* Mail#content_type now returns the content_type field as a string. If you want the mime type of a mail, then you call Mail#mime_type (eg, text/plain), if you want the parameters of the content type field, you call Mail#content_type_parameters which gives you a hash, eg {'format' => 'flowed', 'charset' => 'utf-8'}
* ActionMailer::Base :default_implicit_parts_order now is in the sequence of the order you want, no reversing of ordering takes place. The default order now is text/plain, then text/enriched, then text/html and then any other part that is not one of these three.
* Mail does not have "quoted_body", "quoted_subject" etc. All of these are accessed via body.encoded, subject.encoded etc
* Every object in a Mail object returns an object, never a string. So Mail.body returns a Mail::Body class object, need to call #encoded or #decoded to get the string you want.
* Mail::Message#set_content_type does not exist, it is simply Mail::Message#content_type
* Every mail message gets a unique message_id unless you specify one, had to change all the tests that check for equality with expected.encoded == actual.encoded to first replace their message_ids with control values
* Mail now has a proper concept of parts, remove the ActionMailer::Part and ActionMailer::PartContainer classes
* Calling #encoded on any object returns it as a string ready to go into the output stream of an email, this means it includes the \r\n at the end of the lines and the object is pre-wrapped with \r\n\t if it is a header field. Also, the "encoded" value includes the field name if it is a header field.
* Attachments are only the actual attachment, with filename etc. A part contains an attachment. The part has the content_type etc. So attachments.last.content_type is invalid. But parts.last.content_type
* There is no idea of a "sub_head" in Mail. A part is just a Message with some extra functionality, so it just has a "header" like a normal mail message
*2.3.2 [Final] (March 15, 2009)*
* Fixed that ActionMailer should send correctly formatted Return-Path in MAIL FROM for SMTP #1842 [Matt Jones]

@ -23,7 +23,6 @@ task :default => [ :test ]
Rake::TestTask.new { |t|
t.libs << "test"
t.pattern = 'test/*_test.rb'
t.verbose = true
t.warning = true
}

@ -11,6 +11,7 @@
s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 3.0.pre')
s.add_dependency('mail', '~> 1.5.0')
s.files = Dir['CHANGELOG', 'README', 'MIT-LICENSE', 'lib/**/*']
s.has_rdoc = true

@ -21,36 +21,27 @@
# 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)
actionpack_path = File.expand_path('../../../actionpack/lib', __FILE__)
$:.unshift(actionpack_path) if File.directory?(actionpack_path) && !$:.include?(actionpack_path)
require 'action_controller'
require 'action_view'
module ActionMailer
def self.load_all!
[Base, Part, ::Text::Format, ::Net::SMTP]
end
extend ::ActiveSupport::Autoload
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'
autoload :AdvAttrAccessor
autoload :Base
autoload :DeliveryMethod
autoload :DeprecatedBody
autoload :MailHelper
autoload :Quoting
autoload :TestCase
autoload :TestHelper
end
module Text
extend ActiveSupport::Autoload
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'

@ -1,29 +1,25 @@
module ActionMailer
module AdvAttrAccessor #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
def adv_attr_accessor(*names)
names.each do |name|
ivar = "@#{name}"
module ClassMethods #:nodoc:
def adv_attr_accessor(*names)
names.each do |name|
ivar = "@#{name}"
define_method("#{name}=") do |value|
instance_variable_set(ivar, value)
class_eval <<-ACCESSORS, __FILE__, __LINE__ + 1
def #{name}=(value)
#{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
def #{name}(*args)
raise ArgumentError, "expected 0 or 1 parameters" unless args.length <= 1
if args.empty?
#{ivar} if instance_variable_names.include?(#{ivar.inspect})
else
instance_variable_set(ivar, parameters.first)
#{ivar} = args.first
end
end
end
ACCESSORS
self.protected_instance_variables << ivar if self.respond_to?(:protected_instance_variables)
end
end
end

@ -1,9 +1,10 @@
require 'active_support/core_ext/class'
require 'mail'
require 'action_mailer/tmail_compat'
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.
@ -22,7 +23,8 @@ module ActionMailer #:nodoc:
# bcc ["bcc@example.com", "Order Watcher <watcher@example.com>"]
# from "system@example.com"
# subject "New account information"
# body :account => recipient
#
# @account = recipient
# end
# end
#
@ -42,13 +44,6 @@ module ActionMailer #:nodoc:
# 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
#
@ -68,7 +63,12 @@ module ActionMailer #:nodoc:
# You can even use Action Pack helpers in these views. For example:
#
# You got a new note!
# <%= truncate(note.body, 25) %>
# <%= truncate(@note.body, 25) %>
#
# If you need to access the subject, from or the recipients in the view, you can do that through mailer object:
#
# You got a new note from <%= mailer.from %>!
# <%= truncate(@note.body, 25) %>
#
#
# = Generating URLs
@ -150,7 +150,7 @@ module ActionMailer #:nodoc:
#
# part "text/plain" do |p|
# p.body = render_message("signup-as-plain", :account => recipient)
# p.transfer_encoding = "base64"
# p.content_transfer_encoding = "base64"
# end
# end
# end
@ -250,29 +250,23 @@ module ActionMailer #:nodoc:
# <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
class Base < AbstractController::Base
include Quoting
extend AdvAttrAccessor
include AbstractController::RenderingController
include AbstractController::Logger
include AbstractController::Rendering
include AbstractController::LocalizedCache
include AbstractController::Layouts
include AbstractController::Helpers
helper MailHelper
if Object.const_defined?(:ActionController)
include ActionController::UrlWriter
end
helper ActionMailer::MailHelper
include ActionController::UrlWriter
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
@ -291,10 +285,16 @@ class Base
@@default_mime_version = "1.0"
cattr_accessor :default_mime_version
@@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ]
# This specifies the order that the parts of a multipart email will be. Usually you put
# text/plain at the top so someone without a MIME capable email reader can read the plain
# text of your email first.
#
# Any content type that is not listed here will be inserted in the order you add them to
# the email after the content types you list here.
@@default_implicit_parts_order = [ "text/plain", "text/enriched", "text/html" ]
cattr_accessor :default_implicit_parts_order
@@protected_instance_variables = []
@@protected_instance_variables = %w(@parts @mail)
cattr_reader :protected_instance_variables
# Specify the BCC addresses for the message
@ -344,24 +344,13 @@ class Base
# 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
adv_attr_accessor :mailer_name
def mailer_name(value = nil)
if value
@mailer_name = value
else
@mailer_name || self.class.mailer_name
end
end
# Expose the internal mail
attr_reader :mail
# Alias controller_path to mailer_name so render :partial in views work.
alias :controller_path :mailer_name
@ -376,6 +365,7 @@ class << self
def mailer_name
@mailer_name ||= name.underscore
end
alias :controller_path :mailer_name
def delivery_method=(method_name)
@delivery_method = ActionMailer::DeliveryMethod.lookup_method(method_name)
@ -411,8 +401,7 @@ def method_missing(method_symbol, *parameters) #:nodoc:
# end
def receive(raw_email)
logger.info "Received mail:\n #{raw_email}" unless logger.nil?
mail = TMail::Mail.parse(raw_email)
mail.base64_decode
mail = Mail.new(raw_email)
new.receive(mail)
end
@ -447,21 +436,47 @@ def matches_dynamic_method?(method_name) #:nodoc:
superclass_delegating_reader :delivery_method
self.delivery_method = :smtp
# 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
if custom_headers = params.delete(:headers)
ActiveSupport::Deprecation.warn('Passing custom headers with :headers => {} is deprecated. ' <<
'Please just pass in custom headers directly.', caller[0,10])
params.merge!(custom_headers)
end
part = Mail::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)
super # Run deprecation hooks
params = { :content_type => params } if String === params
params = { :content_disposition => "attachment",
:content_transfer_encoding => "base64" }.merge(params)
part(params, &block)
end
# 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
def initialize(method_name=nil, *args)
super()
create!(method_name, *parameters) if method_name
process(method_name, *args) 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:
# Process the mailer via the given +method_name+. The body will be
# rendered and a new Mail object created.
def process(method_name, *args)
initialize_defaults(method_name)
__send__(method_name, *parameters)
super
# Create e-mail parts
create_parts
@ -470,11 +485,11 @@ def create!(method_name, *parameters) #:nodoc:
@subject ||= I18n.t(:subject, :scope => [:actionmailer, mailer_name, method_name],
:default => method_name.humanize)
# build the mail object itself
@mail = create_mail
# Build the mail object itself
create_mail
end
# Delivers a TMail::Mail object. By default, it delivers the cached mail
# Delivers a 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)
@ -485,7 +500,7 @@ def deliver!(mail = @mail)
logger.debug "\n#{mail.encoded}"
end
ActiveSupport::Notifications.instrument(:deliver_mail, :mail => @mail) do
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
@ -501,15 +516,14 @@ def deliver!(mail = @mail)
# 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)
def initialize_defaults(method_name) #:nodoc:
@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
@mailer_name ||= self.class.mailer_name.dup
@template ||= method_name
@action_name = @template
@parts ||= []
@headers ||= {}
@ -518,29 +532,18 @@ def initialize_defaults(method_name)
super # Run deprecation hooks
end
def create_parts
def create_parts #:nodoc:
super # Run deprecation hooks
if String === response_body
@parts.unshift Part.new(
:content_type => "text/plain",
:disposition => "inline",
:charset => charset,
:body => response_body
)
@parts.unshift create_inline_part(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)
)
self.class.template_root.find_all(@template, {}, @mailer_name).each do |template|
@parts << create_inline_part(render_to_body(:_template => template), template.mime_type)
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
@ -549,37 +552,19 @@ def create_parts
end
end
def sort_parts(parts, order = [])
order = order.collect { |s| s.downcase }
def create_inline_part(body, mime_type=nil) #:nodoc:
ct = mime_type || "text/plain"
main_type, sub_type = split_content_type(ct.to_s)
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
Mail::Part.new(
:content_type => [main_type, sub_type, {:charset => charset}],
:content_disposition => "inline",
:body => body
)
end
def create_mail
m = TMail::Mail.new
def create_mail #:nodoc:
m = Mail.new
m.subject, = quote_any_if_necessary(charset, subject)
m.to, m.from = quote_any_address_if_necessary(charset, recipients, from)
@ -592,27 +577,43 @@ def create_mail
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)
main_type, sub_type = split_content_type(real_content_type)
if @parts.size == 1 && @parts.first.parts.empty?
m.content_type([main_type, sub_type, ctype_attrs])
m.body = @parts.first.body.encoded
else
@parts.each do |p|
part = (TMail::Mail === p ? p : p.to_mail(self))
m.parts << part
m.add_part(p)
end
m.body.set_sort_order(@implicit_parts_order)
m.body.sort_parts!
if real_content_type =~ /multipart/
ctype_attrs.delete "charset"
m.set_content_type(real_content_type, nil, ctype_attrs)
m.content_type([main_type, sub_type, ctype_attrs])
end
end
m.content_transfer_encoding = '8bit' unless m.body.only_us_ascii?
@mail = m
end
def split_content_type(ct) #:nodoc:
ct.to_s.split("/")
end
def parse_content_type(defaults=nil) #:nodoc:
if @content_type.blank?
[ nil, {} ]
else
ctype, *attrs = @content_type.split(/;\s*/)
attrs = attrs.inject({}) { |h,s| k,v = s.split(/\=/, 2); h[k] = v; h }
[ctype, {"charset" => @charset}.merge(attrs)]
end
end
end
end

@ -1,7 +1,7 @@
require "active_support/core_ext/class"
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'
@ -52,6 +52,5 @@ class Method
superclass_delegating_accessor :settings
self.settings = {}
end
end
end

@ -6,13 +6,13 @@ 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"
:location => defined?(Rails.root) ? "#{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|
mail.destinations.uniq.each do |to|
::File.open(::File.join(settings[:location], to), 'a') { |f| f.write(mail) }
end
end

@ -1,8 +1,9 @@
require 'net/smtp'
module ActionMailer
module DeliveryMethod
# A delivery method implementation which sends via smtp.
class Smtp < Method
self.settings = {
:address => "localhost",
:port => 25,
@ -15,8 +16,7 @@ class Smtp < Method
def perform_delivery(mail)
destinations = mail.destinations
mail.ready_to_send
sender = (mail['return-path'] && mail['return-path'].spec) || mail['from']
sender = (mail['return-path'] && mail['return-path'].address) || 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)
@ -26,6 +26,5 @@ def perform_delivery(mail)
end
end
end
end
end

@ -15,6 +15,14 @@ def initialize_defaults(method_name)
@body ||= {}
end
def attachment(params, &block)
if params[:body]
ActiveSupport::Deprecation.warn('attachment :body => "string" is deprecated. To set the body of an attachment ' <<
'please use :data instead, like attachment :data => "string".', caller[0,10])
params[:data] = params.delete(:body)
end
end
def create_parts
if String === @body
ActiveSupport::Deprecation.warn('body is deprecated. To set the body with a text ' <<

@ -1,17 +1,24 @@
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")
module ActionMailer
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" }
# Make list points stand on their own line
formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
formatted
end
formatted
# Access the mailer instance.
def mailer #:nodoc:
@controller
end
end
end

@ -1,107 +0,0 @@
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

@ -1,55 +0,0 @@
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

@ -43,7 +43,7 @@ def quote_any_if_necessary(charset, *args)
# "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) }
address.map { |a| quote_address_if_necessary(a, charset) }.join(", ")
elsif address =~ /^(\S.*)\s+(<.*>)$/
address = $2
phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)

@ -0,0 +1,25 @@
require "action_mailer"
require "rails"
module ActionMailer
class Railtie < Rails::Railtie
plugin_name :action_mailer
initializer "action_mailer.set_configs" do |app|
app.config.action_mailer.each do |k,v|
ActionMailer::Base.send "#{k}=", v
end
end
# TODO: ActionController::Base.logger should delegate to its own config.logger
initializer "action_mailer.logger" do
ActionMailer::Base.logger ||= Rails.logger
end
initializer "action_mailer.view_paths" do |app|
# TODO: this should be combined with the logic for default config.action_mailer.view_paths
view_path = ActionView::PathSet.type_cast(app.config.view_path, app.config.cache_classes)
ActionMailer::Base.template_root = view_path if ActionMailer::Base.view_paths.blank?
end
end
end

@ -1,4 +1,5 @@
require 'active_support/test_case'
require 'action_mailer/base'
module ActionMailer
class NonInferrableMailerError < ::StandardError
@ -43,8 +44,8 @@ def initialize_test_deliveries
end
def set_expected_mail
@expected = TMail::Mail.new
@expected.set_content_type "text", "plain", { "charset" => charset }
@expected = Mail.new
@expected.content_type ["text", "plain", { "charset" => charset }]
@expected.mime_version = '1.0'
end

@ -0,0 +1,20 @@
module Mail
class Message
def set_content_type(*args)
STDERR.puts("Message#set_content_type is deprecated, please just call Message#content_type with the same arguments.\n#{caller}")
content_type(*args)
end
alias :old_transfer_encoding :transfer_encoding
def transfer_encoding(value = nil)
if value
STDERR.puts("Message#transfer_encoding is deprecated, please call Message#content_transfer_encoding with the same arguments.\n#{caller}")
content_transfer_encoding(value)
else
old_transfer_encoding
end
end
end
end

@ -1,7 +0,0 @@
module ActionMailer
module Utils #:nodoc:
def normalize_new_lines(text)
text.to_s.gsub(/\r\n?/, "\n")
end
end
end

File diff suppressed because it is too large Load Diff

@ -1,5 +0,0 @@
require 'tmail/version'
require 'tmail/mail'
require 'tmail/mailbox'
require 'tmail/core_extensions'
require 'tmail/net'

@ -1,426 +0,0 @@
=begin rdoc
= Address handling class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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 <mikel@lindsaar.net>)
# #=> #<TMail::Address mikel@lindsaar.net>
# email.address
# #=> "mikel@lindsaar.net"
# 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. <mikel@me.com>
# Returns:
# "Mikel L." <mikel@me.com>
#
# Unquoted @ symbol in the display name:
# mikel@me.com <mikel@me.com>
# Returns:
# "mikel@me.com" <mikel@me.com>
#
# 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 'mikel@me.com <mikel@me.com>'
# Just quotes it to: '"mikel@me.com" <mikel@me.com>'
when str =~ /\A([^"].+@.+[^"])\s(<.*?>)\Z/
return "\"#{$1}\" #{$2}"
# This handles cases where 'Mikel A. <mikel@me.com>' which is a trailing
# full stop before the address section. Just quotes it to
# '"Mikel A. <mikel@me.com>"
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 mikel@lindsaar.net>"
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 <mikel@lindsaar.net>")
# email.name
# #=> "Mikel Lindsaar"
def name
@name
end
# Setter method for the name or phrase of the email
#
# For Example:
#
# email = TMail::Address.parse("mikel@lindsaar.net")
# email.name
# #=> nil
# email.name = "Mikel Lindsaar"
# email.to_s
# #=> "Mikel Lindsaar <mikel@me.com>"
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:Mikel@me.com>")
# => #<TMail::Address Mikel@me.com>
# email.to_s
# => "<@sa,@another:Mikel@me.com>"
# 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("mikel@lindsaar.net")
# 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("mikel@lindsaar.net")
# 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("mikel@lindsaar.net")
# email.address
# #=> "mikel@lindsaar.net"
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 <mikel@lindsaar.net>")
# #=> "#<TMail::Address mikel@lindsaar.net>"
# addr2 = TMail::Address.parse("Another <mikel@lindsaar.net>")
# #=> "#<TMail::Address mikel@lindsaar.net>"
# 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("mikel@lindsaar.net")
# email.hash
# #=> 18767598
def hash
@local.hash ^ @domain.hash
end
# Duplicates a TMail::Address object returning the duplicate
#
# addr1 = TMail::Address.parse("mikel@lindsaar.net")
# 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

@ -1,46 +0,0 @@
=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

@ -1,46 +0,0 @@
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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:

@ -1,41 +0,0 @@
#: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:

@ -1,67 +0,0 @@
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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:

@ -1,63 +0,0 @@
#: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:

@ -1,581 +0,0 @@
#--
# = COPYRIGHT:
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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

@ -1,960 +0,0 @@
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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 <mikel@nowhere.com>"
# h = TMail::HeaderField.new_from_port(port, "EvelopeSender")
# h.addrs.to_s #=> "mike@anotherplace.com.au"
# 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

@ -1,9 +0,0 @@
#: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:

File diff suppressed because it is too large Load Diff

@ -1,3 +0,0 @@
#:stopdoc:
require 'tmail/mailbox'
#:startdoc:

@ -1,579 +0,0 @@
=begin rdoc
= Mail class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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: mikel@lindsaar.net
# From: mikel@me.com
# 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 "mikel@test.com.au">
# @mail['to'] #=> #<TMail::AddressHeader "mikel@test.com.au">
#
# You can get the string value of this by passing "to_s" to the query:
# Example:
# @mail['to'].to_s #=> "mikel@test.com.au"
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 # => 'mikel@test.com.au'
# @mail['to'] = 'mikel@elsewhere.org'
# @mail['to'].to_s # => 'mikel@elsewhere.org'
# @mail.encoded # => "To: mikel@elsewhere.org\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'] = 'mikel@elsewhere.org'
# @mail['from'] = 'me@me.com'
# @mail.each_header { |k,v| puts "#{k} = #{v}" }
# # => from = me@me.com
# # => to = mikel@elsewhere.org
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

@ -1,495 +0,0 @@
=begin rdoc
= Mailbox and Mbox interaction class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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

@ -1,6 +0,0 @@
#:stopdoc:
require 'tmail/version'
require 'tmail/mail'
require 'tmail/mailbox'
require 'tmail/core_extensions'
#:startdoc:

@ -1,3 +0,0 @@
#:stopdoc:
require 'tmail/mailbox'
#:startdoc:

@ -1,248 +0,0 @@
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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

@ -1,132 +0,0 @@
=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 <aamine@loveruby.net>
#
# 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:

File diff suppressed because it is too large Load Diff

@ -1,379 +0,0 @@
=begin rdoc
= Port class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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

@ -1,118 +0,0 @@
=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

@ -1,58 +0,0 @@
#: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:

@ -1,49 +0,0 @@
=begin rdoc
= Scanner for TMail
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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:

@ -1,261 +0,0 @@
# scanner_r.rb
#
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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:

@ -1,280 +0,0 @@
# encoding: utf-8
=begin rdoc
= String handling class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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

@ -1,337 +0,0 @@
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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
# #=> "<47bf66845380e_25a8fbb80332@baci.local.tmail>"
# email.to_s
# #=> "Message-Id: <47bf668b633f1_25a8fbb80475@baci.local.tmail>\n\n"
# email.message_id = TMail.new_message_id("lindsaar.net")
# #=> "<47bf668b633f1_25a8fbb80475@lindsaar.net.tmail>"
# email.to_s
# #=> "Message-Id: <47bf668b633f1_25a8fbb80475@lindsaar.net.tmail>\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

@ -1,39 +0,0 @@
#
# version.rb
#
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# 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

@ -1,18 +0,0 @@
# 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

@ -1,12 +1,9 @@
root = File.expand_path('../../..', __FILE__)
begin
require "#{root}/vendor/gems/environment"
require File.expand_path('../../../vendor/gems/environment', __FILE__)
rescue LoadError
$:.unshift("#{root}/activesupport/lib")
$:.unshift("#{root}/actionpack/lib")
end
lib = File.expand_path("#{File.dirname(__FILE__)}/../lib")
lib = File.expand_path('../../lib', __FILE__)
$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib)
require 'rubygems'

@ -1,18 +1,36 @@
require 'abstract_unit'
require 'action_mailer/adv_attr_accessor'
class AdvAttrTest < Test::Unit::TestCase
class AdvAttrTest < ActiveSupport::TestCase
class Person
include ActionMailer::AdvAttrAccessor
cattr_reader :protected_instance_variables
@@protected_instance_variables = []
extend 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
def setup
@person = Person.new
end
assert_raise(ArgumentError) {bob.name 'x', 'y'}
def test_adv_attr
assert_nil @person.name
@person.name 'Bob'
assert_equal 'Bob', @person.name
end
def test_adv_attr_writer
assert_nil @person.name
@person.name = 'Bob'
assert_equal 'Bob', @person.name
end
def test_raise_an_error_with_multiple_args
assert_raise(ArgumentError) { @person.name('x', 'y') }
end
def test_ivar_is_added_to_protected_instnace_variables
assert Person.protected_instance_variables.include?('@name')
end
end

@ -24,7 +24,7 @@ def teardown
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
assert_equal "<img alt=\"Somelogo\" src=\"http://www.example.com/images/somelogo.png\" />", mail.body.to_s.strip
end
def test_asset_host_as_one_arguement_proc
@ -36,7 +36,7 @@ def test_asset_host_as_one_arguement_proc
end
}
mail = AssetHostMailer.deliver_email_with_asset(@recipient)
assert_equal "<img alt=\"Somelogo\" src=\"http://images.example.com/images/somelogo.png\" />", mail.body.strip
assert_equal "<img alt=\"Somelogo\" src=\"http://images.example.com/images/somelogo.png\" />", mail.body.to_s.strip
end
def test_asset_host_as_two_arguement_proc
@ -49,6 +49,6 @@ def test_asset_host_as_two_arguement_proc
}
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
assert_equal "<img alt=\"Somelogo\" src=\"http://www.example.com/images/somelogo.png\" />", mail.body.to_s.strip
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -55,7 +55,7 @@ def name_of_the_mailer_class
class MailerHelperTest < Test::Unit::TestCase
def new_mail( charset="utf-8" )
mail = TMail::Mail.new
mail = Mail.new
mail.set_content_type "text", "plain", { "charset" => charset } if charset
mail
end
@ -90,7 +90,7 @@ def test_use_helper_method
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
assert_match %r{east, and\r\n Juliet}, mail.encoded
end
end

@ -65,63 +65,85 @@ def teardown
def test_should_pickup_default_layout
mail = AutoLayoutMailer.create_hello(@recipient)
assert_equal "Hello from layout Inside", mail.body.strip
assert_equal "Hello from layout Inside", mail.body.to_s.strip
end
def test_should_pickup_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient)
assert_equal "multipart/alternative", mail.content_type
# CHANGED: content_type returns an object
# assert_equal "multipart/alternative", mail.content_type
assert_equal "multipart/alternative", mail.mime_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
# CHANGED: content_type returns an object
# assert_equal 'text/plain', mail.parts.first.content_type
assert_equal 'text/plain', mail.parts.first.mime_type
# CHANGED: body returns an object
# assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
# CHANGED: content_type returns an object
# assert_equal 'text/html', mail.parts.last.content_type
assert_equal 'text/html', mail.parts.last.mime_type
# CHANGED: body returns an object
# assert_equal "Hello from layout text/html multipart", mail.parts.last.body
assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s
end
def test_should_pickup_multipartmixed_layout
mail = AutoLayoutMailer.create_multipart(@recipient, "multipart/mixed")
assert_equal "multipart/mixed", mail.content_type
# CHANGED: content_type returns an object
# assert_equal "multipart/mixed", mail.content_type
assert_equal "multipart/mixed", mail.mime_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
# CHANGED: content_type returns an object
# assert_equal 'text/plain', mail.parts.first.content_type
assert_equal 'text/plain', mail.parts.first.mime_type
# CHANGED: body returns an object
# assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
# CHANGED: content_type returns an object
# assert_equal 'text/html', mail.parts.last.content_type
assert_equal 'text/html', mail.parts.last.mime_type
# CHANGED: body returns an object
# assert_equal "Hello from layout text/html multipart", mail.parts.last.body
assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s
end
def test_should_fix_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient, "text/plain")
assert_equal "multipart/alternative", mail.content_type
assert_equal "multipart/alternative", mail.mime_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/plain', mail.parts.first.mime_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
assert_equal 'text/html', mail.parts.last.mime_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s
end
def test_should_pickup_layout_given_to_render
mail = AutoLayoutMailer.create_spam(@recipient)
assert_equal "Spammer layout Hello, Earth", mail.body.strip
assert_equal "Spammer layout Hello, Earth", mail.body.to_s.strip
end
def test_should_respect_layout_false
mail = AutoLayoutMailer.create_nolayout(@recipient)
assert_equal "Hello, Earth", mail.body.strip
assert_equal "Hello, Earth", mail.body.to_s.strip
end
def test_explicit_class_layout
mail = ExplicitLayoutMailer.create_signup(@recipient)
assert_equal "Spammer layout We do not spam", mail.body.strip
assert_equal "Spammer layout We do not spam", mail.body.to_s.strip
end
def test_explicit_layout_exceptions
mail = ExplicitLayoutMailer.create_logout(@recipient)
assert_equal "You logged out", mail.body.strip
assert_equal "You logged out", mail.body.to_s.strip
end
end

@ -40,13 +40,22 @@ def included_subtemplate(recipient)
from "tester@example.com"
end
def included_old_subtemplate(recipient)
def mailer_accessor(recipient)
recipients recipient
subject "Including another template in the one being rendered"
subject "Mailer Accessor"
from "tester@example.com"
@world = "Earth"
render :inline => "Hello, <%= render \"subtemplate\" %>"
render :inline => "Look, <%= mailer.subject %>!"
end
def no_instance_variable(recipient)
recipients recipient
subject "No Instance Variable"
from "tester@example.com"
silence_warnings do
render :inline => "Look, subject.nil? is <%= @subject.nil? %>!"
end
end
def initialize_defaults(method_name)
@ -71,6 +80,8 @@ def share(recipient)
end
end
# CHANGED: Those tests were changed because body returns an object now
# Instead of mail.body.strip, we should mail.body.to_s.strip
class RenderHelperTest < Test::Unit::TestCase
def setup
set_delivery_method :test
@ -86,27 +97,37 @@ def teardown
def test_implicit_body
mail = RenderMailer.create_implicit_body(@recipient)
assert_equal "Hello there, \n\nMr. test@localhost", mail.body.strip
assert_equal "Hello there, \n\nMr. test@localhost", mail.body.to_s.strip
end
def test_inline_template
mail = RenderMailer.create_inline_template(@recipient)
assert_equal "Hello, Earth", mail.body.strip
assert_equal "Hello, Earth", mail.body.to_s.strip
end
def test_file_template
mail = RenderMailer.create_file_template(@recipient)
assert_equal "Hello there, \n\nMr. test@localhost", mail.body.strip
assert_equal "Hello there, \n\nMr. test@localhost", mail.body.to_s.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
assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test/>", mail.body.to_s.strip
end
def test_included_subtemplate
mail = RenderMailer.deliver_included_subtemplate(@recipient)
assert_equal "Hey Ho, let's go!", mail.body.strip
assert_equal "Hey Ho, let's go!", mail.body.to_s.strip
end
def test_mailer_accessor
mail = RenderMailer.deliver_mailer_accessor(@recipient)
assert_equal "Look, Mailer Accessor!", mail.body.to_s.strip
end
def test_no_instance_variable
mail = RenderMailer.deliver_no_instance_variable(@recipient)
assert_equal "Look, subject.nil? is true!", mail.body.to_s.strip
end
end
@ -125,12 +146,12 @@ def teardown
def test_ordering
mail = FirstMailer.create_share(@recipient)
assert_equal "first mail", mail.body.strip
assert_equal "first mail", mail.body.to_s.strip
mail = SecondMailer.create_share(@recipient)
assert_equal "second mail", mail.body.strip
assert_equal "second mail", mail.body.to_s.strip
mail = FirstMailer.create_share(@recipient)
assert_equal "first mail", mail.body.strip
assert_equal "first mail", mail.body.to_s.strip
mail = SecondMailer.create_share(@recipient)
assert_equal "second mail", mail.body.strip
assert_equal "second mail", mail.body.to_s.strip
end
end

@ -9,7 +9,7 @@ def multipart_with_template_path_with_dots(recipient)
subject "This path has dots"
from "Chad Fowler <chad@chadfowler.com>"
attachment :content_type => "text/plain",
:body => "dots dots dots..."
:data => "dots dots dots..."
end
end
@ -31,6 +31,18 @@ def cancelled_account(recipient)
render :text => "Goodbye, Mr. #{recipient}"
end
def from_with_name
from "System <system@loudthinking.com>"
recipients "root@loudthinking.com"
render :text => "Nothing to see here."
end
def from_without_name
from "system@loudthinking.com"
recipients "root@loudthinking.com"
render :text => "Nothing to see here."
end
def cc_bcc(recipient)
recipients recipient
subject "testing bcc/cc"
@ -95,7 +107,7 @@ def utf8_body(recipient)
cc "Foo áëô îü <extended@example.net>"
bcc "Foo áëô îü <extended@example.net>"
charset "utf-8"
render :text => "åœö blah"
end
@ -143,8 +155,8 @@ def explicitly_multipart_example(recipient, ct=nil)
p.body = "blah"
end
attachment :content_type => "image/jpeg", :filename => "foo.jpg",
:body => "123456789"
attachment :content_type => "image/jpeg", :filename => File.join(File.dirname(__FILE__), "fixtures", "attachments", "foo.jpg"),
:data => "123456789"
render :text => "plain text default"
end
@ -227,12 +239,12 @@ def nested_multipart(recipient)
from "test@example.com"
content_type "multipart/mixed"
part :content_type => "multipart/alternative", :content_disposition => "inline", :headers => { "foo" => "bar" } do |p|
part :content_type => "multipart/alternative", :content_disposition => "inline", "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"
attachment :content_type => "application/octet-stream", :filename => "test.txt", :data => "test abcdefghijklmnopqstuvwxyz"
end
def nested_multipart_with_body(recipient)
@ -251,8 +263,8 @@ def attachment_with_custom_header(recipient)
subject "custom header in attachment"
from "test@example.com"
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' => '<test@test.com>' }
part :content_type => "text/html", :body => 'yo'
attachment :content_type => "image/jpeg", :filename => File.join(File.dirname(__FILE__), "fixtures", "attachments", "test.jpg"), :data => "i am not a real picture", 'Content-ID' => '<test@test.com>'
end
def unnamed_attachment(recipient)
@ -261,7 +273,7 @@ def unnamed_attachment(recipient)
from "test@example.com"
content_type "multipart/mixed"
part :content_type => "text/plain", :body => "hullo"
attachment :content_type => "application/octet-stream", :body => "test abcdefghijklmnopqstuvwxyz"
attachment :content_type => "application/octet-stream", :data => "test abcdefghijklmnopqstuvwxyz"
end
def headers_with_nonalpha_chars(recipient)
@ -289,6 +301,7 @@ def return_path
render :text => "testing"
end
# This tests body calls accepeting a hash, which is deprecated.
def body_ivar(recipient)
recipients recipient
subject "Body as a local variable"
@ -319,10 +332,10 @@ def encode( text, charset="utf-8" )
end
def new_mail( charset="utf-8" )
mail = TMail::Mail.new
mail = Mail.new
mail.mime_version = "1.0"
if charset
mail.set_content_type "text", "plain", { "charset" => charset }
mail.content_type ["text", "plain", { "charset" => charset }]
end
mail
end
@ -346,30 +359,33 @@ def teardown
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 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
assert_equal "multipart/mixed", created.mime_type
assert_equal "multipart/alternative", created.parts[0].mime_type
assert_equal "bar", created.parts[0].header['foo'].to_s
assert_nil created.parts[0].charset
assert_equal "text/plain", created.parts[0].parts[0].mime_type
assert_equal "text/html", created.parts[0].parts[1].mime_type
assert_equal "application/octet-stream", created.parts[1].mime_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
assert_equal "multipart/mixed", created.mime_type
assert_equal "multipart/alternative", created.parts.first.mime_type
assert_equal "text/plain", created.parts.first.parts.first.mime_type
assert_equal "Nothing to see here.", created.parts.first.parts.first.body.to_s
assert_equal "text/html", created.parts.first.parts.second.mime_type
assert_equal "<b>test</b> HTML<br/>", created.parts.first.parts.second.body.to_s
end
def test_attachment_with_custom_header
@ -391,20 +407,30 @@ def test_signed_up
created = nil
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
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
delivered = ActionMailer::Base.deliveries.first
assert_not_nil delivered
expected.message_id = '<123@456>'
delivered.message_id = '<123@456>'
assert_equal expected.encoded, delivered.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
assert_equal "Subject with i18n", ActionMailer::Base.deliveries.first.subject.to_s
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
assert_equal "New Subject!", ActionMailer::Base.deliveries.last.subject.to_s
end
def test_custom_template
@ -418,6 +444,8 @@ def test_custom_template
created = nil
assert_nothing_raised { created = TestMailer.create_custom_template(@recipient) }
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
assert_equal expected.encoded, created.encoded
end
@ -440,8 +468,8 @@ def test_custom_templating_extension
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
assert_equal 'text/plain', created.parts[0].mime_type
assert_equal 'text/html', created.parts[1].mime_type
end
def test_cancelled_account
@ -455,11 +483,17 @@ def test_cancelled_account
created = nil
assert_nothing_raised { created = TestMailer.create_cancelled_account(@recipient) }
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
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
delivered = ActionMailer::Base.deliveries.first
expected.message_id = '<123@456>'
delivered.message_id = '<123@456>'
assert_equal expected.encoded, delivered.encoded
end
def test_cc_bcc
@ -477,6 +511,8 @@ def test_cc_bcc
created = TestMailer.create_cc_bcc @recipient
end
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
@ -484,7 +520,33 @@ def test_cc_bcc
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
delivered = ActionMailer::Base.deliveries.first
expected.message_id = '<123@456>'
delivered.message_id = '<123@456>'
assert_equal expected.encoded, delivered.encoded
end
def test_from_without_name_for_smtp
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_from_without_name
mail = MockSMTP.deliveries.first
assert_not_nil mail
mail, from, to = mail
assert_equal 'system@loudthinking.com', from.to_s
end
def test_from_with_name_for_smtp
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_from_with_name
mail = MockSMTP.deliveries.first
assert_not_nil mail
mail, from, to = mail
assert_equal 'system@loudthinking.com', from.addresses.first
end
def test_reply_to
@ -502,14 +564,23 @@ def test_reply_to
created = TestMailer.create_different_reply_to @recipient
end
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
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
delivered = ActionMailer::Base.deliveries.first
assert_not_nil delivered
expected.message_id = '<123@456>'
delivered.message_id = '<123@456>'
assert_equal expected.encoded, delivered.encoded
end
def test_iso_charset
@ -527,14 +598,23 @@ def test_iso_charset
created = TestMailer.create_iso_charset @recipient
end
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
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
delivered = ActionMailer::Base.deliveries.first
assert_not_nil delivered
expected.message_id = '<123@456>'
delivered.message_id = '<123@456>'
assert_equal expected.encoded, delivered.encoded
end
def test_unencoded_subject
@ -552,14 +632,23 @@ def test_unencoded_subject
created = TestMailer.create_unencoded_subject @recipient
end
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
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
delivered = ActionMailer::Base.deliveries.first
assert_not_nil delivered
expected.message_id = '<123@456>'
delivered.message_id = '<123@456>'
assert_equal expected.encoded, delivered.encoded
end
def test_instances_are_nil
@ -625,7 +714,12 @@ def test_delivery_logs_sent_mail
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}")
expected = TestMailer.logger.debug_contents
actual = "\n#{mail.encoded}"
expected.gsub!(/Message-ID:.*\r\n/, "Message-ID: <123@456>\r\n")
actual.gsub!(/Message-ID:.*\r\n/, "Message-ID: <123@456>\r\n")
assert_equal(expected, actual)
end
def test_unquote_quoted_printable_subject
@ -636,9 +730,9 @@ def test_unquote_quoted_printable_subject
The body
EOF
mail = TMail::Mail.parse(msg)
mail = Mail.new(msg)
assert_equal "testing testing \326\244", mail.subject
assert_equal "=?utf-8?Q?testing_testing_=D6=A4?=", mail.quoted_subject
assert_equal "Subject: =?utf-8?Q?testing_testing_=D6=A4?=\r\n", mail[:subject].encoded
end
def test_unquote_7bit_subject
@ -649,9 +743,9 @@ def test_unquote_7bit_subject
The body
EOF
mail = TMail::Mail.parse(msg)
mail = Mail.new(msg)
assert_equal "this == working?", mail.subject
assert_equal "this == working?", mail.quoted_subject
assert_equal "Subject: this == working?\r\n", mail[:subject].encoded
end
def test_unquote_7bit_body
@ -663,9 +757,9 @@ def test_unquote_7bit_body
The=3Dbody
EOF
mail = TMail::Mail.parse(msg)
assert_equal "The=3Dbody", mail.body.strip
assert_equal "The=3Dbody", mail.quoted_body.strip
mail = Mail.new(msg)
assert_equal "The=3Dbody", mail.body.to_s.strip
assert_equal "The=3Dbody", mail.body.encoded.strip
end
def test_unquote_quoted_printable_body
@ -677,9 +771,9 @@ def test_unquote_quoted_printable_body
The=3Dbody
EOF
mail = TMail::Mail.parse(msg)
assert_equal "The=body", mail.body.strip
assert_equal "The=3Dbody", mail.quoted_body.strip
mail = Mail.new(msg)
assert_equal "The=body", mail.body.to_s.strip
assert_equal "The=3Dbody", mail.body.encoded.strip
end
def test_unquote_base64_body
@ -691,9 +785,9 @@ def test_unquote_base64_body
VGhlIGJvZHk=
EOF
mail = TMail::Mail.parse(msg)
assert_equal "The body", mail.body.strip
assert_equal "VGhlIGJvZHk=", mail.quoted_body.strip
mail = Mail.new(msg)
assert_equal "The body", mail.body.to_s.strip
assert_equal "VGhlIGJvZHk=", mail.body.encoded.strip
end
def test_extended_headers
@ -714,14 +808,22 @@ def test_extended_headers
end
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
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
delivered = ActionMailer::Base.deliveries.first
assert_not_nil delivered
expected.message_id = '<123@456>'
delivered.message_id = '<123@456>'
assert_equal expected.encoded, delivered.encoded
end
def test_utf8_body_is_not_quoted
@ -752,40 +854,40 @@ def test_multiple_utf8_recipients
created = TestMailer.create_utf8_body @recipient
assert_match(/\nFrom: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>\r/, created.encoded)
assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>, Example Recipient <me/, created.encoded)
assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>, \r\n\tExample 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)
assert_match(/Jamis/, TestMailer.received_body.to_s)
end
def test_receive_attachments
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email2")
mail = TMail::Mail.parse(fixture)
mail = Mail.new(fixture)
attachment = mail.attachments.last
assert_equal "smime.p7s", attachment.original_filename
assert_equal "application/pkcs7-signature", attachment.content_type
assert_equal "application/pkcs7-signature", mail.parts.last.mime_type
end
def test_decode_attachment_without_charset
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email3")
mail = TMail::Mail.parse(fixture)
mail = Mail.new(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)
mail = Mail.new(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)
mail = Mail.new(fixture)
assert mail.has_attachments?
assert_equal 1, mail.attachments.length
assert_equal "hello.rb", mail.attachments.first.original_filename
@ -793,19 +895,19 @@ def test_attachment_with_text_type
def test_decode_part_without_content_type
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email4")
mail = TMail::Mail.parse(fixture)
mail = Mail.new(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)
mail = Mail.new(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)
mail = Mail.new(fixture)
assert_nothing_raised { mail.body }
end
@ -827,30 +929,29 @@ def test_implicitly_multipart_with_utf8
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 'multipart/mixed', mail.mime_type
assert_equal "text/plain", mail.parts[0].mime_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 "text/html", mail.parts[1].mime_type
assert_equal "iso-8859-1", mail.parts[1].charset
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")
assert_equal "image/jpeg", mail.parts[2].mime_type
assert_equal "attachment", mail.parts[2][:content_disposition].disposition_type
assert_equal "foo.jpg", mail.parts[2][:content_disposition].filename
assert_equal "foo.jpg", mail.parts[2][:content_type].filename
assert_nil mail.parts[2].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
assert_equal "multipart/alternative", mail.mime_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
assert_equal 'multipart/mixed', mail.mime_type
end
def test_implicitly_multipart_messages
@ -858,14 +959,14 @@ def test_implicitly_multipart_messages
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")
assert_equal "1.0", mail.mime_version.to_s
assert_equal "multipart/alternative", mail.mime_type
assert_equal "text/plain", mail.parts[0].mime_type
assert_equal "utf-8", mail.parts[0].charset
assert_equal "text/html", mail.parts[1].mime_type
assert_equal "utf-8", mail.parts[1].charset
assert_equal "application/x-yaml", mail.parts[2].mime_type
assert_equal "utf-8", mail.parts[2].charset
end
def test_implicitly_multipart_messages_with_custom_order
@ -873,41 +974,43 @@ def test_implicitly_multipart_messages_with_custom_order
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
assert_equal "application/x-yaml", mail.parts[0].mime_type
assert_equal "text/plain", mail.parts[1].mime_type
assert_equal "text/html", mail.parts[2].mime_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 "multipart/alternative", mail.header['content-type'].content_type
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")
assert_equal 'iso-8859-1', mail.parts[0].content_type_parameters[:charset]
assert_equal 'iso-8859-1', mail.parts[1].content_type_parameters[:charset]
assert_equal 'iso-8859-1', mail.parts[2].content_type_parameters[:charset]
end
def test_html_mail
mail = TestMailer.create_html_mail(@recipient)
assert_equal "text/html", mail.content_type
assert_equal "text/html", mail.mime_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
assert_equal %{<a href="http://google.com" target="_blank">_Google</a>}, mail.body.to_s
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)
"line #5\n\nline#6\n\nline #7", mail.body.to_s)
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
assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body.to_s
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.to_s
assert_equal "line #1\r\nline #2\r\nline #3\r\nline #4\r\n\r\n", mail.parts[0].body.encoded
assert_equal "<p>line #1</p>\r\n<p>line #2</p>\r\n<p>line #3</p>\r\n<p>line #4</p>\r\n\r\n", mail.parts[1].body.encoded
end
def test_headers_removed_on_smtp_delivery
@ -935,35 +1038,41 @@ def test_file_delivery_should_create_a_file
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
mail = Mail.new(fixture)
assert_equal(2, mail.parts.length)
assert_equal(4, mail.parts.first.parts.length)
assert_equal("This is the first part.", mail.parts.first.parts.first.body.to_s)
assert_equal("test.rb", mail.parts.first.parts.second.filename)
assert_equal("flowed", mail.parts.first.parts.fourth.content_type_parameters[:format])
assert_equal('smime.p7s', mail.parts.second.filename)
end
def test_decode_encoded_attachment_filename
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email8")
mail = TMail::Mail.parse(fixture)
mail = Mail.new(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) }
if expected.respond_to?(:force_encoding)
result = attachment.original_filename.dup
expected.force_encoding(Encoding::ASCII_8BIT)
result.force_encoding(Encoding::ASCII_8BIT)
assert_equal expected, result
else
assert_equal expected, attachment.original_filename
end
end
def test_decode_message_with_unknown_charset
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email10")
mail = TMail::Mail.parse(fixture)
mail = Mail.new(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-Type: application/octet-stream;}, result
assert_match %r{Content-Disposition: attachment[^;]}, result
end
@ -972,9 +1081,9 @@ def test_headers_with_nonalpha_chars
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)
assert_match(/:/, mail[:from].decoded)
assert_match(/:/, mail[:cc].decoded)
assert_match(/:/, mail[:bcc].decoded)
end
def test_deliver_with_mail_object
@ -986,31 +1095,32 @@ def test_deliver_with_mail_object
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 "text/plain", mail.parts[1].mime_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
assert_match %r{format=flowed}, mail.content_type
assert_match %r{charset=utf-8}, mail.content_type
end
def test_return_path_with_create
mail = TestMailer.create_return_path
assert_equal "<another@somewhere.test>", mail['return-path'].to_s
assert_equal "another@somewhere.test", 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: <another@somewhere.test>}, MockSMTP.deliveries[0][0]
assert_match %r{^Return-Path: another@somewhere.test}, MockSMTP.deliveries[0][0]
assert_equal "another@somewhere.test", 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
mail = nil
ActiveSupport::Deprecation.silence { mail = TestMailer.create_body_ivar(@recipient) }
assert_equal "body: foo\nbar: baz", mail.body.to_s
end
def test_starttls_is_enabled_if_supported
@ -1139,6 +1249,6 @@ def test_should_still_raise_exception_with_expected_message_when_calling_an_unde
RespondToMailer.not_a_method
end
assert_match /undefined method.*not_a_method/, error.message
assert_match(/undefined method.*not_a_method/, error.message)
end
end

@ -0,0 +1,24 @@
require 'abstract_unit'
class MailTest < Test::Unit::TestCase
def test_body
m = Mail.new
expected = 'something_with_underscores'
m.content_transfer_encoding = 'quoted-printable'
quoted_body = [expected].pack('*M')
m.body = quoted_body
assert_equal "something_with_underscores=\r\n", m.body.encoded
# CHANGED: body returns object, not string, Changed m.body to m.body.to_s
assert_equal expected, m.body.to_s
end
def test_nested_attachments_are_recognized_correctly
fixture = File.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_nested_attachment")
mail = Mail.new(fixture)
assert_equal 2, mail.attachments.length
assert_equal "image/png", mail.attachments.first.mime_type
assert_equal 1902, mail.attachments.first.decoded.length
assert_equal "application/pkcs7-signature", mail.attachments.last.mime_type
end
end

@ -6,37 +6,37 @@ 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')
b = Mail::Encodings.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')
b = Mail::Encodings.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')
b = Mail::Encodings.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')
b = Mail::Encodings.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')
b = Mail::Encodings.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')
b = Mail::Encodings.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
@ -50,14 +50,17 @@ def test_quote_multibyte_chars
$:.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)
unquoted = Mail::Encodings.unquote_and_convert_to(result, nil)
unquoted.force_encoding(Encoding::ASCII_8BIT) if unquoted.respond_to?(:force_encoding)
original.force_encoding(Encoding::ASCII_8BIT) if original.respond_to?(:force_encoding)
assert_equal unquoted, original
end
@ -65,12 +68,16 @@ def test_quote_multibyte_chars
# 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
mail = Mail.new(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_quoted_with_0d0a"))
# CHANGED: subject returns an object now
# assert_match %r{Elapsed time}, mail.body
assert_match %r{Elapsed time}, mail.body.to_s
end
def test_email_with_partially_quoted_subject
mail = TMail::Mail.parse(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_partially_quoted_subject"))
mail = Mail.new(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_partially_quoted_subject"))
# CHANGED: subject returns an object now
# assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject
assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject
end

@ -18,9 +18,9 @@ def test_setup_sets_right_action_mailer_options
end
def test_setup_creates_the_expected_mailer
assert @expected.is_a?(TMail::Mail)
assert @expected.is_a?(Mail::Message)
assert_equal "1.0", @expected.mime_version
assert_equal "text/plain", @expected.content_type
assert_equal "text/plain", @expected.mime_type
end
def test_mailer_class_is_correctly_inferred
@ -92,7 +92,7 @@ def test_assert_emails_too_few_sent
end
end
assert_match /2 .* but 1/, error.message
assert_match(/2 .* but 1/, error.message)
end
def test_assert_emails_too_many_sent
@ -103,7 +103,7 @@ def test_assert_emails_too_many_sent
end
end
assert_match /1 .* but 2/, error.message
assert_match(/1 .* but 2/, error.message)
end
def test_assert_no_emails_failure
@ -113,7 +113,7 @@ def test_assert_no_emails_failure
end
end
assert_match /0 .* but 1/, error.message
assert_match(/0 .* but 1/, error.message)
end
end
@ -125,7 +125,7 @@ def setup
end
def test_setup_shouldnt_conflict_with_mailer_setup
assert @expected.is_a?(TMail::Mail)
assert @expected.is_a?(Mail::Message)
assert_equal 'a value', @test_var
end
end

@ -0,0 +1,23 @@
require 'abstract_unit'
class TmailCompatTest < Test::Unit::TestCase
def test_set_content_type_raises_deprecation_warning
mail = Mail.new
STDERR.expects(:puts) # Deprecation warning
assert_nothing_raised do
mail.set_content_type "text/plain"
end
assert_equal mail.mime_type, "text/plain"
end
def test_transfer_encoding_raises_deprecation_warning
mail = Mail.new
STDERR.expects(:puts) # Deprecation warning
assert_nothing_raised do
mail.transfer_encoding "base64"
end
assert_equal mail.content_transfer_encoding, "base64"
end
end

@ -1,22 +0,0 @@
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

@ -1,7 +1,9 @@
require 'abstract_unit'
class TestMailer < ActionMailer::Base
class WelcomeController < ActionController::Base
end
class TestMailer < ActionMailer::Base
default_url_options[:host] = 'www.basecamphq.com'
def signed_up_with_url(recipient)
@ -10,8 +12,8 @@ def signed_up_with_url(recipient)
@from = "system@loudthinking.com"
@sent_on = Time.local(2004, 12, 12)
@body["recipient"] = recipient
@body["welcome_url"] = url_for :host => "example.com", :controller => "welcome", :action => "greeting"
@recipient = recipient
@welcome_url = url_for :host => "example.com", :controller => "welcome", :action => "greeting"
end
class <<self
@ -31,10 +33,10 @@ def encode( text, charset="utf-8" )
end
def new_mail( charset="utf-8" )
mail = TMail::Mail.new
mail = Mail.new
mail.mime_version = "1.0"
if charset
mail.set_content_type "text", "plain", { "charset" => charset }
mail.content_type ["text", "plain", { "charset" => charset }]
end
mail
end
@ -52,27 +54,31 @@ def teardown
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 = "system@loudthinking.com"
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
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 = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_signed_up_with_url(@recipient) }
assert_not_nil created
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_signed_up_with_url(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
delivered = ActionMailer::Base.deliveries.first
delivered.message_id = '<123@456>'
assert_equal expected.encoded, delivered.encoded
end
end

@ -1,5 +1,19 @@
*Edge*
* Added ActionDispatch::Request#authorization to access the http authentication header regardless of its proxy hiding [DHH]
* Added :alert, :notice, and :flash as options to ActionController::Base#redirect_to that'll automatically set the proper flash before the redirection [DHH]. Examples:
flash[:notice] = 'Post was created'
redirect_to(@post)
...becomes:
redirect_to(@post, :notice => 'Post was created')
* Added ActionController::Base#notice/= and ActionController::Base#alert/= as a convenience accessors in both the controller and the view for flash[:notice]/= and flash[:alert]/= [DHH]
* Introduce grouped_collection_select helper. #1249 [Dan Codeape, Erik Ostrom]
* Make sure javascript_include_tag/stylesheet_link_tag does not append ".js" or ".css" onto external urls. #1664 [Matthew Rudy Jacobs]

@ -31,7 +31,6 @@ Rake::TestTask.new(:test_action_pack) do |t|
# this will not happen automatically and the tests (as a whole) will error
t.test_files = Dir.glob('test/{abstract,controller,dispatch,template}/**/*_test.rb').sort
t.verbose = true
# t.warning = true
end
@ -45,7 +44,6 @@ desc 'ActiveRecord Integration Tests'
Rake::TestTask.new(:test_active_record_integration) do |t|
t.libs << 'test'
t.test_files = Dir.glob("test/activerecord/*_test.rb")
t.verbose = true
end
# Genereate the RDoc documentation

@ -16,9 +16,9 @@
s.add_dependency('activesupport', '= 3.0.pre')
s.add_dependency('activemodel', '= 3.0.pre')
s.add_dependency('rack', '~> 1.0.1')
s.add_dependency('rack', '~> 1.1.0')
s.add_dependency('rack-test', '~> 0.5.0')
s.add_dependency('rack-mount', '~> 0.0.1')
s.add_dependency('rack-mount', '~> 0.4.0')
s.add_dependency('erubis', '~> 2.6.5')
s.require_path = 'lib'

@ -1,16 +1,18 @@
require "active_support/core_ext/module/attr_internal"
require "active_support/core_ext/module/delegation"
activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
require 'active_support/ruby/shim'
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"
extend ActiveSupport::Autoload
autoload :Base
autoload :Callbacks
autoload :Helpers
autoload :Layouts
autoload :LocalizedCache
autoload :Logger
autoload :Rendering
end

@ -1,8 +1,11 @@
module AbstractController
class Error < StandardError; end
class ActionNotFound < StandardError; end
class Base
attr_internal :response_body
attr_internal :action_name
attr_internal :formats
class << self
attr_reader :abstract
@ -69,26 +72,43 @@ def action_methods
# And always exclude explicitly hidden actions
hidden_actions
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 controller_path
@controller_path ||= name && name.sub(/Controller$/, '').underscore
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)
def process(action, *args)
@_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)
@_response_body = nil
process_action(action_name, *args)
end
# Delegates to the class' #controller_path
def controller_path
self.class.controller_path
end
private
@ -108,8 +128,8 @@ def action_method?(name)
# 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)
def process_action(method_name, *args)
send_action(method_name, *args)
end
# Actually call the method associated with the action. Override

@ -1,12 +0,0 @@
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

@ -4,7 +4,7 @@ module AbstractController
module Helpers
extend ActiveSupport::Concern
include RenderingController
include Rendering
def self.next_serial
@helper_serial ||= 0

@ -1,8 +1,164 @@
module AbstractController
# 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 RenderingController
include Rendering
included do
extlib_inheritable_accessor(:_layout_conditions) { Hash.new }
@ -20,7 +176,7 @@ def inherited(klass)
end
def clear_template_caches!
@found_layouts.clear if @found_layouts
@found_layouts.clear if defined? @found_layouts
super
end
@ -89,7 +245,7 @@ def layout(layout, conditions = {})
# ==== Returns
# String:: A template name
def _implied_layout_name
name && name.underscore
controller_path
end
# Takes the specified layout and creates a _layout method to be called

@ -1,6 +1,6 @@
module AbstractController
class HashKey
@hash_keys = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = {} } }
@hash_keys = Hash.new {|h,k| h[k] = Hash.new {|sh,sk| sh[sk] = {} } }
def self.get(klass, formats, locale)
@hash_keys[klass][formats][locale] ||= new(klass, formats, locale)

@ -9,53 +9,5 @@ module Logger
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

@ -1,13 +1,18 @@
require "abstract_controller/logger"
require "abstract_controller/base"
module AbstractController
module RenderingController
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
module Rendering
extend ActiveSupport::Concern
include AbstractController::Logger
included do
attr_internal :formats
extlib_inheritable_accessor :_view_paths
self._view_paths ||= ActionView::PathSet.new
end
@ -21,7 +26,7 @@ def initialize(*) #:nodoc:
# 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
# View.for_controller[controller] Create a new ActionView instance for a
# controller
# View#render_partial[options]
# - responsible for setting options[:_template]
@ -59,7 +64,7 @@ def render(*args)
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)
_render_partial(options)
else
_determine_template(options)
_render_template(options)
@ -71,7 +76,7 @@ def render_to_body(options = {})
#
# :api: plugin
def render_to_string(options = {})
AbstractController::RenderingController.body_to_s(render_to_body(options))
AbstractController::Rendering.body_to_s(render_to_body(options))
end
# Renders the template from an object.
@ -79,11 +84,18 @@ def render_to_string(options = {})
# ==== 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
# Renders the given partial.
#
# ==== Options
# partial<String|Object>:: The partial name or the object to be rendered
def _render_partial(options)
view_context.render_partial(options)
end
# The list of view paths for this controller. See ActionView::ViewPathSet for
# more details about writing custom view paths.
def view_paths
@ -115,10 +127,10 @@ def self.body_to_s(body)
# _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)
options[:_template] = ActionView::Template::Text.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, {})
handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb")
template = ActionView::Template.new(options[:inline], "inline template", handler, {})
options[:_template] = template
elsif options.key?(:template)
options[:_template_name] = options[:template]
@ -152,12 +164,12 @@ def format_for_text
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
# 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
@ -166,8 +178,8 @@ def append_view_path(path)
# 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
# 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!
@ -186,9 +198,8 @@ def view_paths
# 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)
self._view_paths = paths.is_a?(ActionView::PathSet) ? paths : ActionView::Base.process_view_paths(paths)
end
end
end
end
end

@ -1,66 +1,76 @@
activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
require 'active_support/ruby/shim'
module ActionController
autoload :Base, "action_controller/base"
autoload :Benchmarking, "action_controller/metal/benchmarking"
autoload :ConditionalGet, "action_controller/metal/conditional_get"
autoload :Configuration, "action_controller/metal/configuration"
autoload :Head, "action_controller/metal/head"
autoload :Helpers, "action_controller/metal/helpers"
autoload :HideActions, "action_controller/metal/hide_actions"
autoload :Layouts, "action_controller/metal/layouts"
autoload :Metal, "action_controller/metal"
autoload :Middleware, "action_controller/middleware"
autoload :RackConvenience, "action_controller/metal/rack_convenience"
autoload :Rails2Compatibility, "action_controller/metal/compatibility"
autoload :Redirector, "action_controller/metal/redirector"
autoload :RenderingController, "action_controller/metal/rendering_controller"
autoload :RenderOptions, "action_controller/metal/render_options"
autoload :Rescue, "action_controller/metal/rescuable"
autoload :Responder, "action_controller/metal/responder"
autoload :Session, "action_controller/metal/session"
autoload :Testing, "action_controller/metal/testing"
autoload :UrlFor, "action_controller/metal/url_for"
extend ActiveSupport::Autoload
autoload :Caching, 'action_controller/caching'
autoload :Dispatcher, 'action_controller/dispatch/dispatcher'
autoload :Integration, 'action_controller/deprecated/integration_test'
autoload :IntegrationTest, 'action_controller/deprecated/integration_test'
autoload :MimeResponds, 'action_controller/metal/mime_responds'
autoload :PerformanceTest, 'action_controller/deprecated/performance_test'
autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes'
autoload :RecordIdentifier, 'action_controller/record_identifier'
autoload :Routing, 'action_controller/deprecated'
autoload :SessionManagement, 'action_controller/metal/session_management'
autoload :TestCase, 'action_controller/testing/test_case'
autoload :TestProcess, 'action_controller/testing/process'
autoload :UrlRewriter, 'action_controller/url_rewriter'
autoload :UrlWriter, 'action_controller/url_rewriter'
autoload :Base
autoload :Caching
autoload :PolymorphicRoutes
autoload :Translation
autoload :Metal
autoload :Middleware
autoload :Verification, 'action_controller/metal/verification'
autoload :Flash, 'action_controller/metal/flash'
autoload :RequestForgeryProtection, 'action_controller/metal/request_forgery_protection'
autoload :Streaming, 'action_controller/metal/streaming'
autoload :HttpAuthentication, 'action_controller/metal/http_authentication'
autoload :FilterParameterLogging, 'action_controller/metal/filter_parameter_logging'
autoload :Translation, 'action_controller/translation'
autoload :Cookies, 'action_controller/metal/cookies'
autoload_under "metal" do
autoload :Compatibility
autoload :ConditionalGet
autoload :Configuration
autoload :Cookies
autoload :FilterParameterLogging
autoload :Flash
autoload :Head
autoload :Helpers
autoload :HideActions
autoload :HttpAuthentication
autoload :Logger
autoload :MimeResponds
autoload :RackDelegation
autoload :Redirecting
autoload :Rendering
autoload :Renderers
autoload :RequestForgeryProtection
autoload :Rescue
autoload :Responder
autoload :SessionManagement
autoload :Streaming
autoload :UrlFor
autoload :Verification
end
autoload :ActionControllerError, 'action_controller/metal/exceptions'
autoload :RenderError, 'action_controller/metal/exceptions'
autoload :RoutingError, 'action_controller/metal/exceptions'
autoload :MethodNotAllowed, 'action_controller/metal/exceptions'
autoload :NotImplemented, 'action_controller/metal/exceptions'
autoload :UnknownController, 'action_controller/metal/exceptions'
autoload :MissingFile, 'action_controller/metal/exceptions'
autoload :RenderError, 'action_controller/metal/exceptions'
autoload :SessionOverflowError, 'action_controller/metal/exceptions'
autoload :UnknownHttpMethod, 'action_controller/metal/exceptions'
autoload :Dispatcher, 'action_controller/dispatch/dispatcher'
autoload :PerformanceTest, 'action_controller/deprecated/performance_test'
autoload :Routing, 'action_controller/deprecated'
autoload :Integration, 'action_controller/deprecated/integration_test'
autoload :IntegrationTest, 'action_controller/deprecated/integration_test'
eager_autoload do
autoload :RecordIdentifier
autoload :UrlRewriter
autoload :UrlWriter, 'action_controller/url_rewriter'
# TODO: Don't autoload exceptions, setup explicit
# requires for files that need them
autoload_at "action_controller/metal/exceptions" do
autoload :ActionControllerError
autoload :RenderError
autoload :RoutingError
autoload :MethodNotAllowed
autoload :NotImplemented
autoload :UnknownController
autoload :MissingFile
autoload :RenderError
autoload :SessionOverflowError
autoload :UnknownHttpMethod
end
end
end
autoload :HTML, 'action_controller/vendor/html-scanner'
autoload :AbstractController, 'abstract_controller'
# All of these simply register additional autoloads
require 'abstract_controller'
require 'action_dispatch'
require 'action_view'
require 'action_controller/vendor/html-scanner'
# Common ActiveSupport usage in ActionController
require "active_support/concern"

@ -3,31 +3,28 @@ class Base < Metal
abstract!
include AbstractController::Callbacks
include AbstractController::Logger
include AbstractController::Layouts
include ActionController::Helpers
include ActionController::HideActions
include ActionController::UrlFor
include ActionController::Redirector
include ActionController::RenderingController
include ActionController::RenderOptions::All
include ActionController::Layouts
include ActionController::Redirecting
include ActionController::Rendering
include ActionController::Renderers::All
include ActionController::ConditionalGet
include ActionController::RackConvenience
include ActionController::Benchmarking
include ActionController::RackDelegation
include ActionController::Logger
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::Compatibility
include ActionController::Cookies
include ActionController::Session
include ActionController::Flash
include ActionController::Verification
include ActionController::RequestForgeryProtection
@ -91,7 +88,7 @@ def _normalize_options(action = nil, options = {}, &blk)
end
if options[:status]
options[:status] = interpret_status(options[:status]).to_i
options[:status] = Rack::Utils.status_code(options[:status])
end
options[:update] = blk if block_given?
@ -107,62 +104,5 @@ 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

@ -30,12 +30,15 @@ module ActionController #:nodoc:
# config.action_controller.cache_store = MyOwnStore.new("parameter")
module Caching
extend ActiveSupport::Concern
extend ActiveSupport::Autoload
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'
eager_autoload do
autoload :Actions
autoload :Fragments
autoload :Pages
autoload :Sweeper, 'action_controller/caching/sweeping'
autoload :Sweeping, 'action_controller/caching/sweeping'
end
included do
@@cache_store = nil
@ -57,6 +60,17 @@ module ClassMethods
def cache_configured?
perform_caching && cache_store
end
def log_event(name, before, after, instrumenter_id, payload)
if name.to_s =~ /(read|write|cache|expire|exist)_(fragment|page)\??/
key_or_path = payload[:key] || payload[:path]
human_name = name.to_s.humanize
duration = (after - before) * 1000
logger.info("#{human_name} #{key_or_path.inspect} (%.1fms)" % duration)
else
super
end
end
end
def caching_allowed?

@ -74,7 +74,7 @@ def fragment_exist?(key, options = nil)
return unless cache_configured?
key = fragment_cache_key(key)
ActiveSupport::Notifications.instrument(:fragment_exist?, :key => key) do
ActiveSupport::Notifications.instrument(:exist_fragment?, :key => key) do
cache_store.exist?(key, options)
end
end

@ -13,11 +13,6 @@ def define_dispatcher_callbacks(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)

@ -28,24 +28,9 @@ 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
# implementation includes RackDelegation, which makes a request
# and response object available. You might wish to control the
# environment and response manually for performance reasons.
@ -57,7 +42,7 @@ def initialize(*)
end
# Basic implementations for content_type=, location=, and headers are
# provided to reduce the dependency on the RackConvenience module
# provided to reduce the dependency on the RackDelegation module
# in Renderer and Redirector.
def content_type=(type)
@ -68,6 +53,10 @@ def location=(url)
headers["Location"] = url
end
def status=(status)
@_status = Rack::Utils.status_code(status)
end
# :api: private
def dispatch(name, env)
@_env = env
@ -81,7 +70,7 @@ def to_a
end
class ActionEndpoint
@@endpoints = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = {} } }
@@endpoints = Hash.new {|h,k| h[k] = Hash.new {|sh,sk| sh[sk] = {} } }
def self.for(controller, action, stack)
@@endpoints[controller][action][stack] ||= begin
@ -92,6 +81,7 @@ def self.for(controller, action, stack)
def initialize(controller, action)
@controller, @action = controller, action
@_formats = [Mime::HTML]
end
def call(env)

@ -1,73 +0,0 @@
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

@ -1,5 +1,5 @@
module ActionController
module Rails2Compatibility
module Compatibility
extend ActiveSupport::Concern
class ::ActionController::ActionControllerError < StandardError #:nodoc:
@ -46,11 +46,8 @@ class << self
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
@ -116,7 +113,7 @@ 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

@ -2,7 +2,7 @@ module ActionController
module ConditionalGet
extend ActiveSupport::Concern
include RackConvenience
include RackDelegation
include Head
# Sets the etag, last_modified, or both on the response and renders a

@ -46,17 +46,18 @@ module ActionController #:nodoc:
module Cookies
extend ActiveSupport::Concern
include RackConvenience
include RackDelegation
included do
helper_method :cookies
cattr_accessor :cookie_verifier_secret
end
protected
# Returns the cookie container, which operates as described above.
def cookies
@cookies ||= CookieJar.build(request, response)
end
protected
# Returns the cookie container, which operates as described above.
def cookies
@cookies ||= CookieJar.build(request, response)
end
end
class CookieJar < Hash #:nodoc:
@ -86,7 +87,7 @@ def []=(key, options)
end
super(key.to_s, value)
options[:path] ||= "/"
response.set_cookie(key, options)
end
@ -101,5 +102,96 @@ def delete(key, options = {})
response.delete_cookie(key, options)
value
end
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
#
# cookies.permanent[:prefers_open_id] = true
# # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
#
# This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
#
# This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
#
# cookies.permanent.signed[:remember_me] = current_user.id
# # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
def permanent
@permanent ||= PermanentCookieJar.new(self)
end
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
# cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
# be raised.
#
# This jar requires that you set a suitable secret for the verification on ActionController::Base.cookie_verifier_secret.
#
# Example:
#
# cookies.signed[:discount] = 45
# # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
#
# cookies.signed[:discount] # => 45
def signed
@signed ||= SignedCookieJar.new(self)
end
end
class PermanentCookieJar < CookieJar #:nodoc:
def initialize(parent_jar)
@parent_jar = parent_jar
end
def []=(key, options)
if options.is_a?(Hash)
options.symbolize_keys!
else
options = { :value => options }
end
options[:expires] = 20.years.from_now
@parent_jar[key] = options
end
def signed
@signed ||= SignedCookieJar.new(self)
end
def controller
@parent_jar.controller
end
def method_missing(method, *arguments, &block)
@parent_jar.send(method, *arguments, &block)
end
end
class SignedCookieJar < CookieJar #:nodoc:
def initialize(parent_jar)
unless ActionController::Base.cookie_verifier_secret
raise "You must set ActionController::Base.cookie_verifier_secret to use signed cookies"
end
@parent_jar = parent_jar
@verifier = ActiveSupport::MessageVerifier.new(ActionController::Base.cookie_verifier_secret)
end
def [](name)
@verifier.verify(@parent_jar[name])
end
def []=(key, options)
if options.is_a?(Hash)
options.symbolize_keys!
options[:value] = @verifier.generate(options[:value])
else
options = { :value => @verifier.generate(options) }
end
@parent_jar[key] = options
end
def method_missing(method, *arguments, &block)
@parent_jar.send(method, *arguments, &block)
end
end
end

@ -18,18 +18,9 @@ class MethodNotAllowed < ActionControllerError #:nodoc:
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

@ -2,8 +2,6 @@ module 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.
@ -54,23 +52,25 @@ def filter_parameter_logging(*filter_words, &block)
end
protected :filter_parameters
end
protected
# Overwrite log_process_action to include parameters information.
# If this method is invoked, it means logger is defined, so don't
# worry with such scenario here.
def log_process_action(controller) #:nodoc:
params = controller.send(:filter_parameters, controller.request.params)
logger.info " Parameters: #{params.inspect}" unless params.empty?
super
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
params.dup.except!(*INTERNAL_PARAMS)
end
end

@ -28,7 +28,9 @@ module ActionController #:nodoc:
module Flash
extend ActiveSupport::Concern
include Session
included do
helper_method :alert, :notice
end
class FlashNow #:nodoc:
def initialize(flash)
@ -121,30 +123,18 @@ def store(session)
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
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
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
@ -158,5 +148,54 @@ def flash #:doc:
@_flash
end
# Convenience accessor for flash[:alert]
def alert
flash[:alert]
end
# Convenience accessor for flash[:alert]=
def alert=(message)
flash[:alert] = message
end
# Convenience accessor for flash[:notice]
def notice
flash[:notice]
end
# Convenience accessor for flash[:notice]=
def notice=(message)
flash[:notice] = message
end
protected
def process_action(method_name)
@_flash = nil
super
@_flash.store(session) if @_flash
@_flash = nil
end
def reset_session
super
@_flash = nil
end
def redirect_to(options = {}, response_status_and_flash = {}) #:doc:
if alert = response_status_and_flash.delete(:alert)
flash[:alert] = alert
end
if notice = response_status_and_flash.delete(:notice)
flash[:notice] = notice
end
if other_flashes = response_status_and_flash.delete(:flash)
flash.update(other_flashes)
end
super(options, response_status_and_flash)
end
end
end

@ -1,5 +1,7 @@
module ActionController
module Head
include UrlFor
# 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
@ -21,7 +23,10 @@ def head(status, options = {})
headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s
end
render :nothing => true, :status => status, :location => location
self.status = status
self.location = url_for(location) if location
self.content_type = Mime[formats.first]
self.response_body = " "
end
end
end

@ -52,7 +52,7 @@ module Helpers
included do
# Set the default directory for helpers
extlib_inheritable_accessor(:helpers_dir) do
defined?(Rails) ? "#{Rails.root}/app/helpers" : "app/helpers"
defined?(Rails.root) ? "#{Rails.root}/app/helpers" : "app/helpers"
end
end

@ -1,171 +0,0 @@
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

@ -0,0 +1,89 @@
require 'abstract_controller/logger'
module ActionController
# Adds instrumentation to <tt>process_action</tt> and a <tt>log_event</tt> method
# responsible to log events from ActiveSupport::Notifications. This module handles
# :process_action and :render_template events but allows any other module to hook
# into log_event and provide its own logging facilities (as in ActionController::Caching).
module Logger
extend ActiveSupport::Concern
included do
include AbstractController::Logger
end
attr_internal :view_runtime
def process_action(action)
ActiveSupport::Notifications.instrument(:process_action, :controller => self, :action => action) do
super
end
end
def render(*args, &block)
if logger
render_output = nil
self.view_runtime = cleanup_view_runtime do
Benchmark.ms { render_output = super }
end
render_output
else
super
end
end
# If you want to remove any time taken into account in :view_runtime
# wrongly, you can do it here:
#
# def cleanup_view_runtime
# super - time_taken_in_something_expensive
# end
#
# :api: plugin
def cleanup_view_runtime #:nodoc:
yield
end
module ClassMethods
# This is the hook invoked by ActiveSupport::Notifications.subscribe.
# If you need to log any event, overwrite the method and do it here.
def log_event(name, before, after, instrumenter_id, payload) #:nodoc:
if name == :process_action
duration = [(after - before) * 1000, 0.01].max
controller = payload[:controller]
request = controller.request
logger.info "\n\nProcessed #{controller.class.name}##{payload[:action]} " \
"to #{request.formats} (for #{request.remote_ip} at #{before.to_s(:db)}) " \
"[#{request.method.to_s.upcase}]"
log_process_action(controller)
message = "Completed in %.0fms" % duration
message << " | #{controller.response.status}"
message << " [#{request.request_uri rescue "unknown"}]"
logger.info(message)
elsif name == :render_template
# TODO Make render_template logging work if you are using just ActionView
duration = (after - before) * 1000
message = "Rendered #{payload[:identifier]}"
message << " within #{payload[:layout]}" if payload[:layout]
message << (" (%.1fms)" % duration)
logger.info(message)
end
end
protected
# A hook which allows logging what happened during controller process action.
# :api: plugin
def log_process_action(controller) #:nodoc:
view_runtime = controller.send :view_runtime
logger.info(" View runtime: %.1fms" % view_runtime.to_f) if view_runtime
end
end
end
end

@ -1,8 +1,12 @@
require 'action_dispatch/http/request'
require 'action_dispatch/http/response'
module ActionController
module RackConvenience
module RackDelegation
extend ActiveSupport::Concern
included do
delegate :session, :to => "@_request"
delegate :headers, :status=, :location=, :content_type=,
:status, :location, :content_type, :to => "@_response"
attr_internal :request
@ -23,5 +27,9 @@ def response_body=(body)
response.body = body if response
super
end
def reset_session
@_request.reset_session
end
end
end

@ -0,0 +1,90 @@
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 Redirecting
extend ActiveSupport::Concern
include AbstractController::Logger
# 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
#
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names
# +alert+ and +notice+ as well as a general purpose +flash+ bucket.
#
# Examples:
# redirect_to post_url(@post), :alert => "Watch it, mister!"
# redirect_to post_url(@post), :status=> :found, :notice => "Pay attention to the road"
# redirect_to post_url(@post), :status => 301, :flash => { :updated_post_id => @post.id }
# redirect_to { :action=>'atom' }, :alert => "Something serious happened"
#
# 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?
raise AbstractController::DoubleRenderError if response_body
self.status = _extract_redirect_to_status(options, response_status)
self.location = _compute_redirect_to_location(options)
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>"
logger.info("Redirected to #{location}") if logger && logger.info?
end
private
def _extract_redirect_to_status(options, response_status)
status = if options.is_a?(Hash) && options.key?(:status)
Rack::Utils.status_code(options.delete(:status))
elsif response_status.key?(:status)
Rack::Utils.status_code(response_status[:status])
else
302
end
end
def _compute_redirect_to_location(options)
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.gsub(/[\r\n]/, '')
end
end
end

@ -1,22 +0,0 @@
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

@ -1,103 +0,0 @@
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

@ -0,0 +1,91 @@
module ActionController
def self.add_renderer(key, &block)
Renderers.add(key, &block)
end
module Renderers
extend ActiveSupport::Concern
included do
extlib_inheritable_accessor :_renderers
self._renderers = {}
end
module ClassMethods
def _write_render_options
renderers = _renderers.map do |name, value|
<<-RUBY_EVAL
if options.key?(:#{name})
_process_options(options)
return _render_option_#{name}(options[:#{name}], options)
end
RUBY_EVAL
end
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def _handle_render_options(options)
#{renderers.join}
end
RUBY_EVAL
end
def use_renderers(*args)
args.each do |key|
_renderers[key] = RENDERERS[key]
end
_write_render_options
end
alias use_renderer use_renderers
end
def render_to_body(options)
_handle_render_options(options) || super
end
RENDERERS = {}
def self.add(key, &block)
define_method("_render_option_#{key}", &block)
RENDERERS[key] = block
All._write_render_options
end
module All
extend ActiveSupport::Concern
include Renderers
INCLUDED = []
included do
self._renderers = RENDERERS
_write_render_options
INCLUDED << self
end
def self._write_render_options
INCLUDED.each(&:_write_render_options)
end
end
add :json do |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
add :js do |js, options|
self.content_type ||= Mime::JS
self.response_body = js.respond_to?(:to_js) ? js.to_js : js
end
add :xml do |xml, options|
self.content_type ||= Mime::XML
self.response_body = xml.respond_to?(:to_xml) ? xml.to_xml : xml
end
add :update do |proc, options|
generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(view_context, &proc)
self.content_type = Mime::JS
self.response_body = generator.to_s
end
end
end

@ -1,9 +1,9 @@
module ActionController
module RenderingController
module Rendering
extend ActiveSupport::Concern
included do
include AbstractController::RenderingController
include AbstractController::Rendering
include AbstractController::LocalizedCache
end
@ -20,12 +20,6 @@ def render(options)
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
@ -43,6 +37,12 @@ def _determine_template(options)
super
end
def _render_partial(options)
options[:partial] = action_name if options[:partial] == true
options[:_details] = {:formats => formats}
super
end
def format_for_text
formats.first
end

Some files were not shown because too many files have changed in this diff Show More