mail_safe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+