Permalink
Browse files

initial commit

  • Loading branch information...
1 parent 5bf2a90 commit f254986d825cb25c922f2d5f837076185a9bd60f Marcel Scherf committed Aug 5, 2008
Showing with 600 additions and 0 deletions.
  1. +20 −0 MIT-LICENSE
  2. +79 −0 README.textile
  3. +22 −0 Rakefile
  4. +1 −0 init.rb
  5. +1 −0 install.rb
  6. +37 −0 lib/awesome_email.rb
  7. +42 −0 lib/convert_entities.rb
  8. +82 −0 lib/inline_styles.rb
  9. +72 −0 lib/layouts.rb
  10. +4 −0 tasks/awesome_email_tasks.rake
  11. +201 −0 test/awesome_email_test.rb
  12. +38 −0 test/test_helper.rb
  13. +1 −0 uninstall.rb
View
@@ -0,0 +1,20 @@
+Copyright (c) 2008 imedo GmbH
+
+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.
View
@@ -0,0 +1,79 @@
+h2. For the impatient
+
+Check out the demo application:
+*<code>http://opensource.imedo.de/htmlmail</code>*
+
+Install the plugin:
+*<code>script/plugin install http://github.com/imedo/awesome_email.git</code>*
+
+Learn how to use it below.
+
+h2. Introduction
+
+Have you ever tried sending HTML emails to your users? If you did, you know for sure that it sucks big time: none of the usual ActionView helpers want to work, URL routing is disabled, layouts don't work, and last but not least, the CSS you wrote for your email simply won't work in any e-mail client except maybe Apple Mail. To solve all of the above problems, the <code>awesome_email</code> plugin comes to the rescue. Just install it into your <code>vendor/plugins</code> folder, and the rest comes by itself.
+
+h2. What does it do?
+
+There are a few interesting components in <code>awesome_email</code>:
+
+* awesome_email adds layout support to emails. That means that you can use templates for e-mails just like you would with normal Rails Views.
+* The HTML Mail's CSS is automatcally inlined. That means that your designer and/or CSS guy can design the email in a web browser without worrying about how it might look like in excotic email clients. Yes, it works in Outlook, too, and no, it doesn't work in Outlook 2007 without tweaking. The reason is a "stupid decision from Microsoft about Outlook 2007", but we're working on that one.
+* ConvertEntities replaces Umlauts and other crazy symbols like &auml;, &Ouml; etc. with their HTML Entitiy counterparts e.g. <code>&amp;auml;</code> and so on.
+* HelperMethods allow you to dump the content of the CSS file right into a style tag inside the header of your HTML mail.
+
+h2. How to use it
+
+In your Mailer.delivery_xxx methods you can use
+
+<filter:code>
+ layout "template_filename"
+ css "css_filename"
+</filter:code>
+
+to define which layout should be used and which css file should be used to create inline styles
+
+h3. CSS inlining
+
+The cummulated style of each DOM element will be set as an style attribute when using css inlining.
+
+Example:
+
+ your css file:
+ <filter:code>
+ #some-id { font-size:2em; }
+ .some-class { color:red; }
+ </filter:code>
+
+ your template:
+ <filter:code>
+ <p id="some-id" class="some-class">Hello World!</p>
+ </filter:code>
+
+ will result in the following code:
+ <filter:code>
+ <p id="some-id" class="some-class" style="color:red; font-size:2em;">Hello World!</p>
+ </filter:code>
+
+h2. Important!
+
+Be sure to follow these simple conventions or otherwise awesome_emails's magic will fail:
+
+* The layout must be located inside <code>app/views/layouts/{mailer_name}</code>
+* If you send mutlipart mails, check out the conventions how to name your files: :http://rails.rubyonrails.com/classes/ActionMailer/Base.html
+** So if you have these files inside of /app/views/{mailer_name}: "signup_notification.text.plain.erb", "signup_notification.text.html.erb" ActionMailer will send a multipart mail with two parts: text/plain and text/html
+* Your CSS file must be inside of <code>/public/stylesheets</code>
+
+h2. Dependencies
+
+gems: rails 2.0.2, hpricot, csspool
+
+h2. Getting it, License and Patches
+
+Get the complete source code through :http://github.com/imedo/background. License is MIT. That means that you can do whatever you want with the software, as long as the copyright statement stays intact. Please be a kind open source citizen, and give back your patches and extensions. Just fork the code on Github, and after you're done, send us a pull request. Thanks for your help!
+
+h2. ToDo
+
+* More test coverage (as usual)
+* make it more flexible with view paths
+* rails 2.1 compatibility
+Copyright (c) 2008 imedo GmbH, released under the MIT license
View
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the awesome_email plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the awesome_email plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'AwesomeEmail'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
View
@@ -0,0 +1 @@
+require File.join(File.dirname(__FILE__), 'lib/awesome_email.rb')
View
@@ -0,0 +1 @@
+# Install hook code here
View
@@ -0,0 +1,37 @@
+require File.join(File.dirname(__FILE__), 'layouts.rb')
+require File.join(File.dirname(__FILE__), 'inline_styles.rb')
+require File.join(File.dirname(__FILE__), 'convert_entities.rb')
+
+module AwesomeEmail
+ module Helpers
+
+ # helper methods for ActionView::Base
+ module Views
+ # prints the contents of a file to the page
+ def render_css_file(file_name)
+ File.read(File.join(RAILS_ROOT, "public", file_name)) rescue ""
+ end
+
+ # outputs style sheet information into the header of a webpage
+ # to link the stylesheet absolute, we have to pass in the server_url like: "http://localhost" or "https://localhost:3001"
+ def mail_header_styles(server_url, file)
+ %Q{<link rel="stylesheet" href="#{File.join(server_url, file)}" />\n<style type="text/css"><!-- #{render_css_file(file)} --></style>}
+ end
+ end
+
+ # helper methods für ActionMailer::Base
+ module Mailer
+ protected
+ # sets a few variables that ensure good delivery of the mail
+ def setup_multipart_mail
+ headers 'Content-transfer-encoding' => '8bit'
+ sent_on Time.now
+ content_type 'text/html'
+ end
+ end
+
+ end
+end
+
+ActionView::Base.send(:include, AwesomeEmail::Helpers::Views)
+ActionMailer::Base.send(:include, AwesomeEmail::Helpers::Mailer)
View
@@ -0,0 +1,42 @@
+$KCODE = 'u'
+
+module ActionMailer
+ module ConvertEntities
+ # Add more if replacements you need
+ UMLAUTS = { 'ä' => '&auml;', 'ö' => '&ouml;', 'ü' => '&uuml;', 'Ä' => '&Auml;', 'Ö' => '&Ouml;', 'Ü' => '&Uuml;', 'ß' => '&szlig;' }
+
+ module ClassMethods
+ # none
+ end
+
+ module InstanceMethods
+ # Replace all umlauts
+ # Add more if replacements you need them
+ def convert_to_entities(text)
+ text.gsub(/[#{UMLAUTS.keys.join}]/u) { |match| UMLAUTS[match] }
+ end
+
+ # Convert entities only when rendering html
+ def render_message_with_converted_entities(method_name, body)
+ message = render_message_without_converted_entities(method_name, body)
+ html_part?(method_name) ? convert_to_entities(message) : message
+ end
+
+ # Check if the part we are rendering is html
+ def html_part?(method_name)
+ method_name.gsub(".", "/") =~ /#{Mime::EXTENSION_LOOKUP['html']}/
+ end
+ end
+
+ def self.included(receiver)
+ receiver.extend ClassMethods
+ receiver.send :include, InstanceMethods
+
+ receiver.class_eval do
+ alias_method_chain :render_message, :converted_entities
+ end
+ end
+ end
+end
+
+ActionMailer::Base.send :include, ActionMailer::ConvertEntities
View
@@ -0,0 +1,82 @@
+require 'hpricot'
+require 'csspool'
+
+CSS::SAC::GeneratedParser.send :include, CSS::SAC::Conditions
+
+module ActionMailer
+ module InlineStyles
+ module ClassMethods
+ # none
+ end
+
+ module InstanceMethods
+
+ def inline(html)
+ css_doc = parse_css_doc(build_css_file_name_from_css_setting)
+ html_doc = parse_html_doc(html)
+ render_inline(css_doc, html_doc)
+ end
+
+ def render_message_with_inline_styles(method_name, body)
+ message = render_message_without_inline_styles(method_name, body)
+ return message if @css.blank?
+ inline(message)
+ end
+
+ protected
+
+ def render_inline(css_doc, html_doc)
+ css_doc.find_all_rules_matching(html_doc).each do |rule|
+ inline_css = css_for_rule(rule)
+ (html_doc/rule.selector.to_css).each{|e| e['style'] = inline_css + (e['style']||'') }
+ end
+ html_doc.to_s
+ end
+
+ def css_for_rule(rule)
+ rule.properties.map do |key, value, important|
+ build_css(key, value, important)
+ end.join
+ end
+
+ def build_css(key, value, important)
+ delimiter = (key == 'font-family') ? ', ' : ' '
+ values = [value].flatten.join(delimiter)
+ "#{key}:#{values}#{important ? ' !important' : ''};"
+ end
+
+ def parse_html_doc(html)
+ Hpricot.parse(html)
+ end
+
+ def parse_css_doc(file_name)
+ sac = CSS::SAC::Parser.new
+ sac.parse(parse_css_from_file(file_name))
+ end
+
+ def parse_css_from_file(file_name)
+ File.read(file_name)
+ end
+
+ def build_css_file_name_from_css_setting
+ @css.blank? ? "" : build_css_file_name(@css)
+ end
+
+ def build_css_file_name(css_name)
+ "#{RAILS_ROOT}/public/stylesheets/mails/#{css_name}.css"
+ end
+ end
+
+ def self.included(receiver)
+ receiver.extend ClassMethods
+ receiver.send :include, InstanceMethods
+
+ receiver.class_eval do
+ adv_attr_accessor :css
+ alias_method_chain :render_message, :inline_styles
+ end
+ end
+ end
+end
+
+ActionMailer::Base.send :include, ActionMailer::InlineStyles
View
@@ -0,0 +1,72 @@
+module ActionMailer
+ module Layouts
+ module ClassMethods
+ # none
+ end
+
+ module InstanceMethods
+
+ # render with layout, if it is set through the "layout" accessor method and a corresponding file is found
+ def render_message_with_layouts(method_name, body)
+ return render_message_without_layouts(method_name, body) if @layout.blank?
+ # template was set, now render with layout
+ template = initialize_template_class body
+ template = render_content method_name, template
+ render_layout_template template, method_name
+ end
+
+ protected
+ # tries to find a matching template and renders the inner content back to the template
+ def render_content(method_name, template)
+ template.instance_variable_set(:@content_for_layout, render_content_for_layout(method_name, template))
+ template
+ end
+
+ # builds the filename from the method_name, then renders the inner content
+ def render_content_for_layout(method_name, template)
+ file_name = extend_with_mailer_name(method_name)
+ template.render(:file => file_name)
+ end
+
+ # finds the layout file and renders it, if the file is not found an exception is raised
+ # default path for all mailer layouts is layouts/mailers below app/views/
+ # you can pass in another layout path as 3rd arguments
+ def render_layout_template(template, method_name, layout_path = File.join('layouts', 'mailers'))
+ extension_parts = method_name.split('.')[1..-1]
+ while !extension_parts.blank?
+ file_name = File.join(layout_path, ([@layout.to_s] + extension_parts).join('.'))
+ return render_layout(file_name, template) if template_exists?(file_name)
+ extension_parts.shift
+ end
+ # nothing found, complain
+ raise "Layout '#{@layout}' not found"
+ end
+
+ def render_layout(file_name, template)
+ template.render(file_name)
+ end
+
+ # check if a the given view exists within the app/views folder
+ def template_exists?(file_name)
+ File.exist?(File.join(RAILS_ROOT, 'app', 'views', file_name))
+ end
+
+ def extend_with_mailer_name(template_name)
+ template_name =~ /\// ? template_name : File.join(mailer_name, template_name)
+ end
+ end
+
+ # create "layout" method to define the layout name
+ def self.included(receiver)
+ receiver.extend ClassMethods
+ receiver.send :include, InstanceMethods
+
+ receiver.class_eval do
+ adv_attr_accessor :layout
+ alias_method_chain :render_message, :layouts
+ end
+ end
+ end
+end
+
+ActionMailer::Base.send :include, ActionMailer::Layouts
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :awesome_email do
+# # Task goes here
+# end
Oops, something went wrong.

0 comments on commit f254986

Please sign in to comment.