ar_mailer_aws 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 82b61c67d1d1f4cfe28d311a36f55e82bb346e78
4
+ data.tar.gz: 4aa5150e3f1ec039dc2fee41d023267c36d810ff
5
+ SHA512:
6
+ metadata.gz: e7f857364b01171113bdb0766bf0b77f9299c9725da0b0703267a839d4b56cd40a6e999503060c91ff586a423aac0f0820a7aef01a284ddd7b81f5a796b51c93
7
+ data.tar.gz: 1dc6fc917b265effc0e3f404e510857f37fac5da6f7e521396b8d1178d0d926b1079aba4a50f2c46db4ad0f866e087e3dce6c02becb091382a0d7dee3dd8ba9d
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ .idea
20
+ .rvmrc
21
+ .rspec
22
+ TODO
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ar_mailer_aws.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Alex Leschenko
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # ArMailerAWS
2
+
3
+ Daemon for sending butches of emails via Amazon Simple Email Service (Amazon SES) using ActiveRecord for storing messages
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'ar_mailer_aws'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install ar_mailer_aws
19
+
20
+ Run generator:
21
+
22
+ $ rails g ar_mailer_aws BatchEmail
23
+
24
+ Run migrations:
25
+
26
+ $ rake db:migrate
27
+
28
+ ## Usage
29
+
30
+ Edit config/initializer/ar_mailer_aws.rb and uncomment below line to use ar_mailer as default delivery method:
31
+
32
+ ActionMailer::Base.delivery_method = :ar_mailer_aws
33
+
34
+ Or if you need to, you can set each mailer class delivery method individually:
35
+
36
+ class MyMailer < ActionMailer::Base
37
+ self.delivery_method = :ar_mailer_aws
38
+ end
39
+
40
+ Run delivery daemon:
41
+
42
+ $ bundle exec ar_mailer_aws start
43
+
44
+ To list available options:
45
+
46
+ $ bundle exec ar_mailer_aws --help
47
+
48
+
49
+ ## Contributing
50
+
51
+ 1. Fork it
52
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
53
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
54
+ 4. Push to the branch (`git push origin my-new-feature`)
55
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ar_mailer_aws/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ar_mailer_aws'
8
+ spec.version = ArMailerAWS::VERSION
9
+ spec.authors = ['Alex Leschenko']
10
+ spec.email = %w(leschenko.al@gmail.com)
11
+ spec.description = %q{Daemon for sending butches of emails via Amazon Simple Email Service (Amazon SES) using ActiveRecord for storing messages}
12
+ spec.summary = %q{Send butches of emails via Amazon SES}
13
+ spec.homepage = 'https://github.com/leschenko/ar_mailer_aws'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = %w(lib)
20
+
21
+ spec.add_dependency 'daemons', '~> 1.1.9'
22
+ spec.add_dependency 'aws-sdk', '~> 1.0'
23
+ spec.add_dependency 'activesupport', '>= 3.0'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.3'
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'rspec'
28
+ spec.add_development_dependency 'forgery'
29
+ spec.add_development_dependency 'sqlite3'
30
+ spec.add_development_dependency 'activerecord', '>= 3.0'
31
+ end
data/bin/ar_mailer_aws ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'ar_mailer_aws'
6
+ require 'daemons'
7
+
8
+ require 'ostruct'
9
+ require 'optparse'
10
+
11
+ # require Rails environment expect running tests
12
+ name = File.basename $0
13
+ unless name == 'rspec'
14
+ begin
15
+ require Dir.pwd + '/config/environment'
16
+ ActiveRecord::Base.connection.disconnect!
17
+ rescue LoadError
18
+ <<-EOF
19
+ #{name} must be run from a Rails application's root to deliver email.
20
+ #{Dir.pwd} does not appear to be a Rails application root.
21
+ EOF
22
+ end
23
+ end
24
+
25
+ options = ArMailerAWS.parse_options(ARGV)
26
+ Daemons.run_proc('ar_mailer_aws') do
27
+ if defined? Rails
28
+ ActiveRecord::Base.establish_connection
29
+ Rails.logger = ActiveRecord::Base.logger = ActionMailer::Base.logger = ActiveSupport::BufferedLogger.new(Rails.root.join("log/#{Rails.env}.log"))
30
+ end
31
+ ArMailerAWS.run(options)
32
+ end
@@ -0,0 +1,113 @@
1
+ require 'ar_mailer_aws/version'
2
+ require 'ar_mailer_aws/sender'
3
+ require 'ar_mailer_aws/mailer'
4
+ require 'ar_mailer_aws/railtie' if defined? Rails
5
+ require 'active_support/core_ext'
6
+
7
+ module ArMailerAWS
8
+
9
+ # ActiveRecord class for storing emails
10
+ mattr_accessor :email_class
11
+ @@email_class = 'BatchEmail'
12
+
13
+ # options to AWS::SimpleEmailService initializer
14
+ mattr_accessor :ses_options
15
+ @@ses_options = {}
16
+
17
+ # ar_mailer_aws logger
18
+ mattr_accessor :logger
19
+
20
+ # error proc called when error occurred during delivering an email
21
+ # Example: lambda { |email, exception| ExceptionNotifier::Notifier.background_exception_notification(exception, data: {email: email.attributes}).deliver }
22
+ mattr_accessor :error_proc
23
+
24
+ class << self
25
+ def setup
26
+ yield self
27
+ end
28
+
29
+ def run(options)
30
+ sender = Sender.new(options)
31
+ loop do
32
+ sender.send_batch
33
+ sleep sender.options.delay
34
+ end
35
+ end
36
+
37
+ def parse_options(args)
38
+ start_i = args.index('--').try(:succ) || 0
39
+ args = args[start_i..-1]
40
+ options = OpenStruct.new
41
+
42
+ OptionParser.new do |opts|
43
+ options.batch_size = 100
44
+ options.delay = 180
45
+ options.quota = 10_000
46
+ options.rate = 5
47
+ options.max_age = 3600 * 24 * 7
48
+
49
+ opts.banner = <<-TXT.strip_heredoc
50
+ Usage: ar_mailer_aws <command> <options> -- <application options>
51
+
52
+ * where <command> is one of:
53
+ start start an instance of the application
54
+ stop stop all instances of the application
55
+ restart stop all instances and restart them afterwards
56
+ reload send a SIGHUP to all instances of the application
57
+ run start the application and stay on top
58
+ zap set the application to a stopped state
59
+ status show status (PID) of application instances
60
+
61
+ * where <options> may contain several of the following:
62
+
63
+ -t, --ontop Stay on top (does not daemonize)
64
+ -f, --force Force operation
65
+ -n, --no_wait Do not wait for processes to stop
66
+
67
+ * and where <application options> may contain several of the following:
68
+
69
+ TXT
70
+
71
+ opts.on('-b', '--batch-size BATCH_SIZE', 'Maximum number of emails to send per delay',
72
+ "Default: #{options.batch_size}", Integer) do |batch_size|
73
+ options.batch_size = batch_size
74
+ end
75
+
76
+ opts.on('-d', '--delay DELAY', 'Delay between checks for new mail in the database',
77
+ "Default: #{options.delay}", Integer) do |delay|
78
+ options.delay = delay
79
+ end
80
+
81
+ opts.on('-q', '--quota QUOTA', 'Daily quota for sending emails', "Default: #{options.quota}", Integer) do |quota|
82
+ options.quota = quota
83
+ end
84
+
85
+ opts.on('-r', '--rate RATE', 'Maximum number of emails send per second',
86
+ "Default: #{options.rate}", Integer) do |rate|
87
+ options.rate = rate
88
+ end
89
+
90
+ opts.on('-m', '--max-age MAX_AGE',
91
+ 'Maxmimum age for an email. After this',
92
+ 'it will be removed from the queue.',
93
+ 'Set to 0 to disable queue cleanup.',
94
+ "Default: #{options.max_age} seconds", Integer) do |max_age|
95
+ options.max_age = max_age
96
+ end
97
+
98
+ opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
99
+ options.verbose = v
100
+ end
101
+
102
+ opts.on_tail('-h', '--help', 'Show this message') do
103
+ puts opts
104
+ exit
105
+ end
106
+
107
+ end.parse!(args)
108
+
109
+ options
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,40 @@
1
+ module ArMailerAWS
2
+ class Mailer
3
+
4
+ attr_accessor :email_class
5
+
6
+ def initialize(options={})
7
+ self.email_class = options[:email_class] || ArMailerAWS.email_class.constantize
8
+ end
9
+
10
+ def deliver!(mail)
11
+ envelope_from, destinations, message = check_params(mail)
12
+
13
+ destinations.each do |destination|
14
+ self.email_class.create! mail: message, to: destination, from: envelope_from
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def check_params(mail)
21
+ envelope_from = mail.return_path || mail.sender || mail.from_addrs.first
22
+ if envelope_from.blank?
23
+ raise ArgumentError.new('A sender (Return-Path, Sender or From) required to send a message')
24
+ end
25
+
26
+ destinations ||= mail.destinations if mail.respond_to?(:destinations) && mail.destinations
27
+ if destinations.blank?
28
+ raise ArgumentError.new('At least one recipient (To, Cc or Bcc) is required to send a message')
29
+ end
30
+
31
+ message ||= mail.encoded if mail.respond_to?(:encoded)
32
+ if message.blank?
33
+ raise ArgumentError.new('A encoded content is required to send a message')
34
+ end
35
+
36
+ [envelope_from, destinations, message]
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ module ArMailerAWS
2
+ class Railtie < Rails::Railtie
3
+ initializer 'ar_mailer_aws' do
4
+
5
+ ActiveSupport.on_load :action_mailer do
6
+ add_delivery_method :ar_mailer_aws, ArMailerAWS::Mailer
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,92 @@
1
+ require 'aws-sdk'
2
+
3
+ module ArMailerAWS
4
+ class Sender
5
+ attr_reader :options, :model, :ses
6
+
7
+ def initialize(options={})
8
+ @options = options.is_a?(Hash) ? OpenStruct.new(options) : options
9
+ @model = ArMailerAWS.email_class.constantize
10
+ @ses = AWS::SimpleEmailService.new ArMailerAWS.ses_options
11
+ @day = Date.today
12
+ @sent_count = 0
13
+ end
14
+
15
+ def send_batch
16
+ cleanup
17
+ emails = find_emails
18
+ log "found #{emails.length} emails to deliver"
19
+ send_emails(emails) unless emails.empty?
20
+ end
21
+
22
+ def send_emails(emails)
23
+ sent_per_second = 0
24
+ emails.each do |email|
25
+ if exceed_quota?
26
+ log "exceed daily quota in #{@quota}, sent by mailer #{@sent_count}, other #{@sent_last_24_hours}"
27
+ return
28
+ end
29
+ begin
30
+ if sent_per_second == options.rate
31
+ sleep 1
32
+ sent_per_second = 0
33
+ else
34
+ sent_per_second += 1
35
+ end
36
+ log "send email to #{email.to}"
37
+ @ses.send_raw_email email.mail, from: email.from, to: email.to
38
+ email.destroy
39
+ @sent_count += 1
40
+ rescue => e
41
+ log "ERROR sending email #{email.id} - #{email.inspect}: #{e.message}\n #{e.backtrace.join("\n ")}", :error
42
+ ArMailerAWS.error_proc.call(email, e) if ArMailerAWS.error_proc
43
+ email.update_column(:last_send_attempt_at, Time.now)
44
+ end
45
+ end
46
+ end
47
+
48
+ def exceed_quota?
49
+ if @day == Date.today
50
+ options.quota <= @sent_count + sent_last_24_hours
51
+ else
52
+ @sent_count = 0
53
+ @sent_last_24_hours = nil
54
+ false
55
+ end
56
+ end
57
+
58
+ def sent_last_24_hours
59
+ @sent_last_24_hours ||= begin
60
+ count = @ses.quotas[:sent_last_24_hours]
61
+ log "#{count} emails sent last 24 hours"
62
+ count
63
+ end
64
+ end
65
+
66
+ def find_emails
67
+ @model.where('last_send_attempt_at IS NULL OR last_send_attempt_at < ?', Time.now - 300).limit(options.batch_size)
68
+ end
69
+
70
+ def cleanup
71
+ return if options.max_age.to_i.zero?
72
+ timeout = Time.now - options.max_age
73
+ emails = @model.destroy_all(['last_send_attempt_at IS NOT NULL AND created_at < ?', timeout])
74
+
75
+ log "expired #{emails.length} emails"
76
+ end
77
+
78
+ def log(msg, level=:info)
79
+ formatted_msg = "[#{Time.now}] ar_mailer_aws: #{msg}"
80
+ puts formatted_msg if options.verbose
81
+ if logger
82
+ logger.send(level, msg)
83
+ elsif options.verbose && defined? Rails
84
+ Rails.logger.send(level, formatted_msg)
85
+ end
86
+ end
87
+
88
+ def logger
89
+ ArMailerAWS.logger
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module ArMailerAWS
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,27 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ class ArMailerAwsGenerator < Rails::Generators::NamedBase
5
+ include Rails::Generators::Migration
6
+
7
+ source_root File.expand_path('../templates', __FILE__)
8
+
9
+ def create_ar_mailer_files
10
+ self.class.check_class_collision class_name
11
+ template('ar_mailer_aws.rb', 'config/initializers/ar_mailer_aws.rb')
12
+ template('model.rb', File.join('app/models', class_path, "#{file_name}.rb"))
13
+ migration_template 'migration.rb', "db/migrate/create_#{file_path.gsub(/\//, '_').pluralize}.rb"
14
+ end
15
+
16
+ def self.next_migration_number(dirname)
17
+ if ActiveRecord::Base.timestamped_migrations
18
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
19
+ else
20
+ '%.3d' % (current_migration_number(dirname) + 1)
21
+ end
22
+ end
23
+
24
+ def self.banner
25
+ 'Usage: rails ar_mailer_aws EmailModelName (default: BatchEmail)'
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ AWS.config(
2
+ :access_key_id => 'YOUR_ACCESS_KEY_ID',
3
+ :secret_access_key => 'YOUR_SECRET_ACCESS_KEY'
4
+ )
5
+
6
+ <% if class_name != 'BatchEmail' -%>
7
+ ArMailerAWS.email_class = '<%= class_name %>'
8
+ <% end -%>
9
+
10
+ #ActionMailer::Base.delivery_method = :ar_mailer_aws
@@ -0,0 +1,11 @@
1
+ class <%= migration_class_name.gsub(/::/, '') %> < ActiveRecord::Migration
2
+ def change
3
+ create_table :<%= table_name %> do |t|
4
+ t.string :from
5
+ t.string :to
6
+ t.text :mail, limit: 16777215
7
+ t.datetime :last_send_attempt_at
8
+ t.datetime :created_at
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ class <%= class_name %> < ActiveRecord::Base
2
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe ArMailerAWS do
4
+
5
+ it 'setup yields ArMailerAWS' do
6
+ ArMailerAWS.setup do |config|
7
+ config.should == ArMailerAWS
8
+ end
9
+ end
10
+
11
+ describe '#run', focus: true do
12
+ before do
13
+ @sender = ArMailerAWS::Sender.new(delay: 1)
14
+ ArMailerAWS::Sender.stub(:new).and_return(@sender)
15
+ end
16
+
17
+ it 'run sender' do
18
+ @sender.should_receive(:send_batch).twice
19
+ begin
20
+ Timeout::timeout(2) do
21
+ ArMailerAWS.run({})
22
+ end
23
+ rescue Timeout::Error
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe ArMailerAWS::Mailer do
4
+
5
+ it 'initializer email_class option' do
6
+ mailer = ArMailerAWS::Mailer.new(email_class: CustomEmailClass)
7
+ mailer.email_class.name.should == 'CustomEmailClass'
8
+ end
9
+
10
+ context 'delivering' do
11
+ before do
12
+ @mail = stub('Mail')
13
+ @mail.stub(:return_path).and_return('from@example.com')
14
+ @mail.stub(:destinations).and_return(['to@example.com'])
15
+ @mail.stub(:encoded).and_return('email content')
16
+ @mailer = ArMailerAWS::Mailer.new
17
+ end
18
+
19
+ it '#check_params' do
20
+ params = @mailer.send(:check_params, @mail)
21
+ params[0].should == 'from@example.com'
22
+ params[1].should == ['to@example.com']
23
+ params[2].should == 'email content'
24
+ end
25
+
26
+ it 'store emails into db on deliver!' do
27
+ expect {
28
+ @mailer.deliver!(@mail)
29
+ }.to change { @mailer.email_class.count }.by(1)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'command line options parsing' do
4
+ it 'return defaults if no options specified' do
5
+ options = ArMailerAWS.parse_options([])
6
+ options.batch_size.should == 100
7
+ options.delay.should == 180
8
+ options.quota.should == 10_000
9
+ options.rate.should == 5
10
+ options.max_age.should == 3600 * 24 * 7
11
+ end
12
+
13
+ it 'batch_size' do
14
+ ArMailerAWS.parse_options(%w(-b 10)).batch_size.should == 10
15
+ end
16
+
17
+ it 'delay' do
18
+ ArMailerAWS.parse_options(%w(-d 90)).delay.should == 90
19
+ end
20
+
21
+ it 'quota' do
22
+ ArMailerAWS.parse_options(%w(-q 100)).quota.should == 100
23
+ end
24
+
25
+ it 'rate' do
26
+ ArMailerAWS.parse_options(%w(-r 7)).rate.should == 7
27
+ end
28
+
29
+ it 'max_age' do
30
+ ArMailerAWS.parse_options(%w(-m 300)).max_age.should == 300
31
+ end
32
+
33
+ it 'verbose' do
34
+ ArMailerAWS.parse_options(%w(-v)).verbose.should be_true
35
+ end
36
+ end
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+
3
+ def create_email(options={})
4
+ BatchEmail.create!({from: 'from@example.com', to: 'to@example.com', mail: 'email content'}.update(options))
5
+ end
6
+
7
+ describe ArMailerAWS::Sender do
8
+
9
+ it 'convert Hash options to OpenStruct' do
10
+ sender = ArMailerAWS::Sender.new({})
11
+ sender.options.class.name.should == 'OpenStruct'
12
+ end
13
+
14
+ it 'get default emails model' do
15
+ ArMailerAWS::Sender.new.model.name.should == 'BatchEmail'
16
+ end
17
+
18
+ it 'supply ses options to AWS::SimpleEmailService initializer' do
19
+ ArMailerAWS.ses_options = {a: 1}
20
+ AWS::SimpleEmailService.should_receive(:new).with({a: 1})
21
+ ArMailerAWS::Sender.new
22
+ end
23
+
24
+ context 'sending' do
25
+ before do
26
+ BatchEmail.delete_all
27
+ end
28
+
29
+ describe '#find_emails' do
30
+ it 'batch_size emails' do
31
+ 5.times { create_email }
32
+ @sender = ArMailerAWS::Sender.new(batch_size: 3)
33
+ @sender.find_emails.should have(3).emails
34
+ end
35
+
36
+ it 'ignore emails last_send_attempt_at < 300 seconds ago' do
37
+ 2.times { create_email }
38
+ 2.times { create_email(last_send_attempt_at: Time.now - 100) }
39
+ @sender = ArMailerAWS::Sender.new(batch_size: 3)
40
+ @sender.find_emails.should have(2).emails
41
+ end
42
+ end
43
+
44
+ describe '#cleanup' do
45
+ it 'do nothing if max_age == 0' do
46
+ @sender = ArMailerAWS::Sender.new(max_age: 0)
47
+ @sender.model.should_not_receive(:destroy_all)
48
+ @sender.cleanup
49
+ end
50
+
51
+ it 'remove emails with last_send_attempt_at and create_at greater then max_age' do
52
+ 2.times { create_email }
53
+ 2.times { create_email(last_send_attempt_at: Time.now, created_at: Time.now - 4000) }
54
+
55
+ @sender = ArMailerAWS::Sender.new(max_age: 3600)
56
+ expect {
57
+ @sender.cleanup
58
+ }.to change { @sender.model.count }.from(4).to(2)
59
+ end
60
+ end
61
+
62
+ describe '#send_emails' do
63
+ before do
64
+ @sender = ArMailerAWS::Sender.new(quota: 100)
65
+ @sender.ses.stub(:send_raw_email)
66
+ @sender.ses.stub(:quotas).and_return({sent_last_24_hours: 0})
67
+ end
68
+
69
+ context 'success' do
70
+ it 'send email via ses' do
71
+ 2.times { create_email }
72
+ @sender.ses.should_receive(:send_raw_email).twice
73
+ @sender.send_emails(@sender.model.all)
74
+ end
75
+
76
+ it 'remove sent emails' do
77
+ 2.times { create_email }
78
+ expect {
79
+ @sender.send_emails(@sender.model.all)
80
+ }.to change { @sender.model.count }.from(2).to(0)
81
+ end
82
+ end
83
+
84
+ context 'error' do
85
+ it 'call error_proc' do
86
+ email = create_email
87
+ exception = StandardError.new
88
+ ArMailerAWS.error_proc = proc {}
89
+ ArMailerAWS.error_proc.should_receive(:call).with(email, exception)
90
+ @sender.ses.should_receive(:send_raw_email).and_raise(exception)
91
+ @sender.send_emails([email])
92
+ end
93
+
94
+ it 'update last_send_attempt_at column' do
95
+ email = create_email
96
+ exception = StandardError.new
97
+ @sender.ses.should_receive(:send_raw_email).and_raise(exception)
98
+ @sender.send_emails([email])
99
+ email.reload.last_send_attempt_at.should_not be_nil
100
+ end
101
+ end
102
+
103
+ context 'rate' do
104
+ it 'call not more the rate times per second' do
105
+ 5.times { create_email }
106
+ @sender.options.rate = 2
107
+ @sender.ses.should_receive(:send_raw_email).twice
108
+ begin
109
+ Timeout::timeout(1) do
110
+ @sender.send_emails(@sender.model.all)
111
+ end
112
+ rescue Timeout::Error
113
+ end
114
+ end
115
+ end
116
+
117
+ context 'quota' do
118
+ it 'not exceed quota' do
119
+ 10.times { create_email }
120
+ @sender.options.quota = 5
121
+ expect {
122
+ @sender.send_emails(@sender.model.all)
123
+ }.to change { @sender.model.count }.by(-5)
124
+ end
125
+
126
+ it 'consider sent_last_24_hours from ses' do
127
+ 10.times { create_email }
128
+ @sender.ses.stub(:quotas).and_return({sent_last_24_hours: 10})
129
+ @sender.options.quota = 15
130
+ expect {
131
+ @sender.send_emails(@sender.model.all)
132
+ }.to change { @sender.model.count }.by(-5)
133
+ end
134
+ end
135
+ end
136
+
137
+ describe '#send_batch' do
138
+ before do
139
+ @sender = ArMailerAWS::Sender.new
140
+ @sender.ses.stub(:send_raw_email)
141
+ end
142
+
143
+ it 'no pending emails' do
144
+ @sender.should_receive(:cleanup)
145
+ @sender.should_receive(:find_emails).and_return([])
146
+ @sender.should_not_receive(:send_emails)
147
+ @sender.send_batch
148
+ end
149
+
150
+ it 'no pending emails' do
151
+ @sender.should_receive(:find_emails).and_return([create_email])
152
+ @sender.should_receive(:send_emails)
153
+ @sender.send_batch
154
+ end
155
+ end
156
+
157
+ end
158
+ end
@@ -0,0 +1,29 @@
1
+ require 'active_record'
2
+ require 'active_support/core_ext'
3
+ require 'ar_mailer_aws'
4
+ require 'forgery'
5
+
6
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
7
+
8
+ ActiveRecord::Migration.verbose = false
9
+ ActiveRecord::Schema.define(:version => 1) do
10
+ create_table :batch_emails do |t|
11
+ t.string :from
12
+ t.string :to
13
+ t.text :mail, limit: 16777215
14
+ t.datetime :last_send_attempt_at
15
+ t.datetime :created_at
16
+ end
17
+ end
18
+
19
+ #ActiveRecord::Base.logger = Logger.new(STDERR)
20
+
21
+ ArMailerAWS.setup do |config|
22
+ #config.logger = Logger.new(STDERR)
23
+ end
24
+
25
+ class BatchEmail < ActiveRecord::Base
26
+ end
27
+
28
+ class CustomEmailClass
29
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ar_mailer_aws
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alex Leschenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-07-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: daemons
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.9
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.1.9
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: forgery
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: activerecord
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ description: Daemon for sending butches of emails via Amazon Simple Email Service
140
+ (Amazon SES) using ActiveRecord for storing messages
141
+ email:
142
+ - leschenko.al@gmail.com
143
+ executables:
144
+ - ar_mailer_aws
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - .gitignore
149
+ - .travis.yml
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - README.md
153
+ - Rakefile
154
+ - ar_mailer_aws.gemspec
155
+ - bin/ar_mailer_aws
156
+ - lib/ar_mailer_aws.rb
157
+ - lib/ar_mailer_aws/mailer.rb
158
+ - lib/ar_mailer_aws/railtie.rb
159
+ - lib/ar_mailer_aws/sender.rb
160
+ - lib/ar_mailer_aws/version.rb
161
+ - lib/generators/ar_mailer_aws/ar_mailer_aws_generator.rb
162
+ - lib/generators/ar_mailer_aws/templates/ar_mailer_aws.rb
163
+ - lib/generators/ar_mailer_aws/templates/migration.rb
164
+ - lib/generators/ar_mailer_aws/templates/model.rb
165
+ - spec/ar_mailer_aws/ar_mailer_aws_spec.rb
166
+ - spec/ar_mailer_aws/mailer_spec.rb
167
+ - spec/ar_mailer_aws/parse_options_spec.rb
168
+ - spec/ar_mailer_aws/sender_spec.rb
169
+ - spec/spec_helper.rb
170
+ homepage: https://github.com/leschenko/ar_mailer_aws
171
+ licenses:
172
+ - MIT
173
+ metadata: {}
174
+ post_install_message:
175
+ rdoc_options: []
176
+ require_paths:
177
+ - lib
178
+ required_ruby_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - '>='
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - '>='
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ requirements: []
189
+ rubyforge_project:
190
+ rubygems_version: 2.0.3
191
+ signing_key:
192
+ specification_version: 4
193
+ summary: Send butches of emails via Amazon SES
194
+ test_files:
195
+ - spec/ar_mailer_aws/ar_mailer_aws_spec.rb
196
+ - spec/ar_mailer_aws/mailer_spec.rb
197
+ - spec/ar_mailer_aws/parse_options_spec.rb
198
+ - spec/ar_mailer_aws/sender_spec.rb
199
+ - spec/spec_helper.rb