mail_safe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Myron Marston
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,65 @@
1
+ = mail_safe
2
+
3
+ Mail safe provides a safety net while you're developing an application that uses ActionMailer.
4
+ It keeps emails from escaping into the wild.
5
+
6
+ Once you've installed and configured this gem, you can rest assure that your app won't send
7
+ emails to external email addresses. Instead, emails that would normally be delivered to external
8
+ addresses will be sent to an address of your choosing, and the body of the email will be appended
9
+ with a note stating where the email was originally intended to go.
10
+
11
+ == Download
12
+
13
+ Github: http://github.com/myronmarston/mail_safe/tree/master
14
+
15
+ Gem:
16
+ gem install myronmarston-mail_safe --source http://gems.github.com
17
+
18
+ == Usage
19
+
20
+ Load the gem in your non-production environments using Rails' 2.1+ gem support. For example, I'm loading this in
21
+ config/environments/development.rb and config/environments/staging.rb:
22
+
23
+ config.gem 'myronmarston-mail_safe', :lib => 'mail_safe', :source => 'http://gems.github.com'
24
+
25
+ Be sure not to load this in your production or test environment, otherwise, your emails won't be sent to the proper
26
+ recipients. (The Rails test environment ensures that no emails are ever sent.)
27
+
28
+ Next, configure mail safe. Create a file at config/initializers/mail_safe.rb, similar to the following:
29
+
30
+ if defined?(MailSafe::Config)
31
+ MailSafe::Config.internal_address_definition = /.*@my-domain\.com/i
32
+ MailSafe::Config.replacement_address = 'me@my-domain.com'
33
+ end
34
+
35
+ The internal address definition determines which addresses will be ignored (i.e. sent normally) and which will be replaced. Email being sent to internal
36
+ addresses will be sent normally; all other email addresses will be replaced by the replacement address.
37
+
38
+ These settings can also take procs if you need something more flexible:
39
+
40
+ if defined?(MailSafe::Config)
41
+ # Emails sent to addresses longer than 15 characters long will be sent to the replacement address instead.
42
+ MailSafe::Config.internal_address_definition = lambda { |address| address.size <= 15 }
43
+
44
+ # Useful if your mail server allows + dynamic email addresses like gmail.
45
+ MailSafe::Config.replacement_address = lambda { |address| "my-address+#{address.gsub(/[\w\-.]/, '_')}@gmail.com" }
46
+ end
47
+
48
+ When mail safe replaces an email address, it appends a notice to the bottom of the email body, such as:
49
+
50
+ **************************************************
51
+ This email originally had different recipients,
52
+ but MailSafe has prevented it from being sent to them.
53
+
54
+ The original recipients were:
55
+ - to:
56
+ - external-address-1@domain.com
57
+ - external-address-2@domain.com
58
+ - cc:
59
+ - external-address-3@domain.com
60
+
61
+ **************************************************
62
+
63
+ == Copyright
64
+
65
+ Copyright (c) 2009 Myron Marston, Kashless.org. See LICENSE for details.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 0
4
+ :major: 0
@@ -0,0 +1,8 @@
1
+ require 'active_support'
2
+ require 'action_mailer'
3
+
4
+ require 'mail_safe/config'
5
+ require 'mail_safe/action_mailer'
6
+ require 'mail_safe/address_replacer'
7
+
8
+ ActionMailer::Base.send(:include, MailSafe::ActionMailer) unless ActionMailer::Base.ancestors.include?(MailSafe::ActionMailer)
@@ -0,0 +1,14 @@
1
+ module MailSafe
2
+ module ActionMailer
3
+ def self.included(base)
4
+ base.class_eval do
5
+ alias_method_chain :deliver!, :mail_safe
6
+ end
7
+ end
8
+
9
+ def deliver_with_mail_safe!(mail = @mail)
10
+ MailSafe::AddressReplacer.replace_external_addresses(mail) if mail
11
+ deliver_without_mail_safe!(mail)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,113 @@
1
+ module MailSafe
2
+ class AddressReplacer
3
+ class << self
4
+ include ::ActionMailer::Utils
5
+ ADDRESS_TYPES = [:to, :cc, :bcc].freeze
6
+
7
+ def replace_external_addresses(mail)
8
+ replaced_addresses = {}
9
+
10
+ ADDRESS_TYPES.each do |address_type|
11
+ if addresses = mail.send(address_type)
12
+ new_addresses = []
13
+
14
+ addresses.each do |a|
15
+ new_addresses << if MailSafe::Config.is_internal_address?(a)
16
+ a
17
+ else
18
+ (replaced_addresses[address_type] ||= []) << a
19
+ MailSafe::Config.get_replacement_address(a)
20
+ end
21
+ end
22
+
23
+ mail.send("#{address_type}=", new_addresses)
24
+ end
25
+ end
26
+
27
+ self.add_body_postscript(mail, replaced_addresses)
28
+ end
29
+
30
+ protected
31
+
32
+ def add_body_postscript(part, replaced_addresses)
33
+ return unless replaced_addresses.size > 0
34
+
35
+ case part.content_type
36
+ when 'text/plain' then add_text_postscript(part, replaced_addresses)
37
+ when 'text/html' then add_html_postscript(part, replaced_addresses)
38
+ end
39
+
40
+ part.parts.each { |p| add_body_postscript(p, replaced_addresses) }
41
+ end
42
+
43
+ def add_text_postscript(part, replaced_addresses)
44
+ address_type_postscripts = []
45
+ ADDRESS_TYPES.each do |address_type|
46
+ next unless addresses = replaced_addresses[address_type]
47
+ address_type_postscripts << "- #{address_type}:\n - #{addresses.join("\n - ")}"
48
+ end
49
+
50
+ postscript = <<-EOS
51
+
52
+
53
+ **************************************************
54
+ This email originally had different recipients,
55
+ but MailSafe has prevented it from being sent to them.
56
+
57
+ The original recipients were:
58
+ #{address_type_postscripts.join("\n\n")}
59
+
60
+ **************************************************
61
+ EOS
62
+
63
+ set_part_body(part, part.body + postscript)
64
+ end
65
+
66
+ def add_html_postscript(part, replaced_addresses)
67
+ address_type_postscripts = []
68
+ ADDRESS_TYPES.each do |address_type|
69
+ next unless addresses = replaced_addresses[address_type]
70
+ address_type_postscripts << "#{address_type}:<ul>\n<li>#{addresses.join("</li>\n<li>")}</li>\n</ul>"
71
+ end
72
+
73
+ postscript = <<-EOS
74
+ <div class="mail-safe-postscript">
75
+ <hr />
76
+
77
+ <p>
78
+ This email originally had different recipients,
79
+ but MailSafe has prevented it from being sent to them.
80
+ </p>
81
+
82
+ <p>
83
+ The original recipients were:
84
+ </p>
85
+
86
+ <ul>
87
+ <li>
88
+ #{address_type_postscripts.join("</li>\n<li>")}
89
+ </li>
90
+ </ul>
91
+
92
+ <hr/ >
93
+ </div>
94
+ EOS
95
+
96
+ set_part_body(part, part.body + postscript)
97
+ end
98
+
99
+ def set_part_body(part, body)
100
+ # taken from action mailer:
101
+ # http://github.com/rails/rails/blob/05d7409ae5fd423be6f747ad553f659fcecbf548/actionmailer/lib/action_mailer/part.rb#L58-65
102
+ case part.content_transfer_encoding.to_s.downcase
103
+ when "base64" then
104
+ part.body = TMail::Base64.folding_encode(body)
105
+ when "quoted-printable"
106
+ part.body = [normalize_new_lines(body)].pack("M*")
107
+ else
108
+ part.body = body
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,25 @@
1
+ module MailSafe
2
+ class InvalidConfigSettingError < StandardError; end
3
+
4
+ class Config
5
+ cattr_accessor :internal_address_definition
6
+
7
+ def self.is_internal_address?(address)
8
+ case internal_address_definition
9
+ when Regexp then address =~ internal_address_definition
10
+ when Proc then internal_address_definition.call(address)
11
+ else raise InvalidConfigSettingError.new("internal_address_definition must be a Regexp or Proc, but was: #{internal_address_definition.class.to_s}")
12
+ end
13
+ end
14
+
15
+ cattr_accessor :replacement_address
16
+
17
+ def self.get_replacement_address(original_address)
18
+ case replacement_address
19
+ when String then replacement_address
20
+ when Proc then replacement_address.call(original_address)
21
+ else raise InvalidConfigSettingError.new("replacement_address must be a String or Proc, but was: #{replacement_address.class.to_s}")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,75 @@
1
+ require 'test_helper'
2
+
3
+ class ConfigTest < Test::Unit::TestCase
4
+ context 'When internal_address_definition is set to a regexp, #is_internal_address?' do
5
+ setup do
6
+ MailSafe::Config.internal_address_definition = /.*@example\.com/
7
+ end
8
+
9
+ should 'return true if the address matches the regexp' do
10
+ assert MailSafe::Config.is_internal_address?('someone@example.com')
11
+ end
12
+
13
+ should 'return false if the address does not match the regexp' do
14
+ assert !MailSafe::Config.is_internal_address?('someone@another-domain.com')
15
+ end
16
+ end
17
+
18
+ context 'When internal_address_definition is set to a lambda, #is_internal_address?' do
19
+ setup do
20
+ MailSafe::Config.internal_address_definition = lambda { |address| address.size < 15 }
21
+ end
22
+
23
+ should 'return true if the lambda returns true for the given address' do
24
+ assert MailSafe::Config.is_internal_address?('abc@foo.com')
25
+ end
26
+
27
+ should 'return false if the lambda returns false for the given address' do
28
+ assert !MailSafe::Config.is_internal_address?('a-long-address@example.com')
29
+ end
30
+ end
31
+
32
+ context 'When internal_address_definition is not set, #is_internal_address?' do
33
+ setup do
34
+ MailSafe::Config.internal_address_definition = nil
35
+ end
36
+
37
+ should 'raise an error' do
38
+ assert_raise MailSafe::InvalidConfigSettingError do
39
+ MailSafe::Config.is_internal_address?('abc@foo.com')
40
+ end
41
+ end
42
+ end
43
+
44
+ context 'When replacement_address is set to a string, #get_replacement_address' do
45
+ setup do
46
+ MailSafe::Config.replacement_address = 'me@mydomain.com'
47
+ end
48
+
49
+ should 'return the configured replacement address' do
50
+ assert_equal 'me@mydomain.com', MailSafe::Config.get_replacement_address('you@example.com')
51
+ end
52
+ end
53
+
54
+ context 'When replacement_address is set to a proc, #get_replacement_address' do
55
+ setup do
56
+ MailSafe::Config.replacement_address = lambda { |address| "me+#{address.split('@').first}@mydomain.com" }
57
+ end
58
+
59
+ should 'return the configured replacement address' do
60
+ assert_equal 'me+you@mydomain.com', MailSafe::Config.get_replacement_address('you@example.com')
61
+ end
62
+ end
63
+
64
+ context 'When replacement_address is not set, #get_replacement_address' do
65
+ setup do
66
+ MailSafe::Config.replacement_address = nil
67
+ end
68
+
69
+ should 'raise an error' do
70
+ assert_raise MailSafe::InvalidConfigSettingError do
71
+ MailSafe::Config.get_replacement_address('you@example.com')
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,88 @@
1
+ require 'test_helper'
2
+
3
+ class MailerTest < Test::Unit::TestCase
4
+ TEXT_POSTSCRIPT_PHRASE = /The original recipients were:/
5
+ HTML_POSTSCRIPT_PHRASE = /<p>\s+The original recipients were:\s+<\/p>/
6
+
7
+ context 'Delivering a plain text email to internal addresses' do
8
+ setup do
9
+ MailSafe::Config.stubs(:is_internal_address? => true)
10
+ @email = TestMailer.deliver_plain_text_message(:to => 'internal-to@address.com', :bcc => 'internal-bcc@address.com', :cc => 'internal-cc@address.com')
11
+ end
12
+
13
+ should 'send the email to the original addresses' do
14
+ assert_equal ['internal-to@address.com'], @email.to
15
+ assert_equal ['internal-cc@address.com'], @email.cc
16
+ assert_equal ['internal-bcc@address.com'], @email.bcc
17
+ end
18
+
19
+ should 'not add a post script to the body' do
20
+ assert_no_match TEXT_POSTSCRIPT_PHRASE, @email.body
21
+ end
22
+ end
23
+
24
+ context 'Delivering a plain text email to external addresses' do
25
+ setup do
26
+ MailSafe::Config.stubs(:is_internal_address? => false, :get_replacement_address => 'replacement@example.com')
27
+ @email = TestMailer.deliver_plain_text_message(:to => 'internal-to@address.com', :bcc => 'internal-bcc@address.com', :cc => 'internal-cc@address.com')
28
+ end
29
+
30
+ should 'send the email to the replacement address' do
31
+ assert_equal ['replacement@example.com'], @email.to
32
+ assert_equal ['replacement@example.com'], @email.cc
33
+ assert_equal ['replacement@example.com'], @email.bcc
34
+ end
35
+ end
36
+
37
+ def deliver_email_with_mix_of_internal_and_external_addresses(delivery_method)
38
+ MailSafe::Config.internal_address_definition = /internal/
39
+ MailSafe::Config.replacement_address = 'internal@domain.com'
40
+ @email = TestMailer.send(delivery_method,
41
+ {
42
+ :to => ['internal1@address.com', 'external1@address.com'],
43
+ :cc => ['internal1@address.com', 'internal2@address.com'],
44
+ :bcc => ['external1@address.com', 'external2@address.com']
45
+ }
46
+ )
47
+ end
48
+
49
+ context 'Delivering a plain text email to a mix of internal and external addresses' do
50
+ setup do
51
+ deliver_email_with_mix_of_internal_and_external_addresses(:deliver_plain_text_message)
52
+ end
53
+
54
+ should 'send the email to the appropriate address' do
55
+ assert_same_elements ['internal1@address.com', 'internal@domain.com'], @email.to
56
+ assert_same_elements ['internal1@address.com', 'internal2@address.com'], @email.cc
57
+ assert_same_elements ['internal@domain.com', 'internal@domain.com'], @email.bcc
58
+ end
59
+
60
+ should 'add a plain text post script to the body' do
61
+ assert_match TEXT_POSTSCRIPT_PHRASE, @email.body
62
+ end
63
+ end
64
+
65
+ context 'Delivering an html email to a mix of internal and external addresses' do
66
+ setup do
67
+ deliver_email_with_mix_of_internal_and_external_addresses(:deliver_html_message)
68
+ end
69
+
70
+ should 'add an html post script to the body' do
71
+ assert_match HTML_POSTSCRIPT_PHRASE, @email.body
72
+ end
73
+ end
74
+
75
+ context 'Delivering a multipart email to a mix of internal and external addresses' do
76
+ setup do
77
+ deliver_email_with_mix_of_internal_and_external_addresses(:deliver_multipart_message)
78
+ end
79
+
80
+ should 'add an text post script to the body of the text part' do
81
+ assert_match TEXT_POSTSCRIPT_PHRASE, @email.parts.detect { |p| p.content_type == 'text/plain' }.body
82
+ end
83
+
84
+ should 'add an html post script to the body of the html part' do
85
+ assert_match HTML_POSTSCRIPT_PHRASE, @email.parts.detect { |p| p.content_type == 'text/html' }.body
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,38 @@
1
+ class TestMailer < ActionMailer::Base
2
+ # template root must be set for multipart emails, or ActionMailer will throw an exception.
3
+ self.template_root = File.dirname(__FILE__)
4
+
5
+ def plain_text_message(options)
6
+ setup_recipients(options)
7
+ from 'test@mailsafe.org'
8
+ subject "Plain text Message Test"
9
+ body "Here is the message body."
10
+ end
11
+
12
+ def html_message(options)
13
+ setup_recipients(options)
14
+ from 'test@mailsafe.org'
15
+ subject "Html Message Test"
16
+ body "<p>Here is the message body.</p>"
17
+ content_type 'text/html'
18
+ end
19
+
20
+ def multipart_message(options)
21
+ setup_recipients(options)
22
+ from 'test@mailsafe.org'
23
+ subject "Html Message Test"
24
+
25
+ content_type 'multipart/alternative'
26
+
27
+ part :content_type => 'text/plain', :body => "Here is the message body."
28
+ part :content_type => 'text/html', :body => "<p>Here is the message body.</p>"
29
+ end
30
+
31
+ protected
32
+
33
+ def setup_recipients(options)
34
+ recipients options[:to]
35
+ cc options[:cc]
36
+ bcc options[:bcc]
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+
6
+ begin
7
+ require 'ruby-debug'
8
+ Debugger.start
9
+ Debugger.settings[:autoeval] = true if Debugger.respond_to?(:settings)
10
+ rescue LoadError
11
+ # ruby-debug wasn't available so neither can the debugging be
12
+ end
13
+
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+ require 'mail_safe'
17
+ require 'mailers/test_mailer'
18
+
19
+ ActionMailer::Base.delivery_method = :test
20
+
21
+ class Test::Unit::TestCase
22
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mail_safe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Myron Marston
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-28 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: actionmailer
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: Shoulda
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: mocha
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ description:
56
+ email: myron.marston@gmail.com
57
+ executables: []
58
+
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - README.rdoc
63
+ - LICENSE
64
+ files:
65
+ - README.rdoc
66
+ - VERSION.yml
67
+ - lib/mail_safe/action_mailer.rb
68
+ - lib/mail_safe/address_replacer.rb
69
+ - lib/mail_safe/config.rb
70
+ - lib/mail_safe.rb
71
+ - test/config_test.rb
72
+ - test/mailer_test.rb
73
+ - test/mailers/test_mailer.rb
74
+ - test/test_helper.rb
75
+ - LICENSE
76
+ has_rdoc: true
77
+ homepage: http://github.com/myronmarston/mail_safe
78
+ licenses: []
79
+
80
+ post_install_message:
81
+ rdoc_options:
82
+ - --inline-source
83
+ - --charset=UTF-8
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ version:
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ requirements: []
99
+
100
+ rubyforge_project:
101
+ rubygems_version: 1.3.5
102
+ signing_key:
103
+ specification_version: 2
104
+ summary: Keep your ActionMailer emails from escaping into the wild during development.
105
+ test_files: []
106
+