ar_mailer_aws 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +6 -0
- data/ar_mailer_aws.gemspec +31 -0
- data/bin/ar_mailer_aws +32 -0
- data/lib/ar_mailer_aws.rb +113 -0
- data/lib/ar_mailer_aws/mailer.rb +40 -0
- data/lib/ar_mailer_aws/railtie.rb +11 -0
- data/lib/ar_mailer_aws/sender.rb +92 -0
- data/lib/ar_mailer_aws/version.rb +3 -0
- data/lib/generators/ar_mailer_aws/ar_mailer_aws_generator.rb +27 -0
- data/lib/generators/ar_mailer_aws/templates/ar_mailer_aws.rb +10 -0
- data/lib/generators/ar_mailer_aws/templates/migration.rb +11 -0
- data/lib/generators/ar_mailer_aws/templates/model.rb +2 -0
- data/spec/ar_mailer_aws/ar_mailer_aws_spec.rb +28 -0
- data/spec/ar_mailer_aws/mailer_spec.rb +32 -0
- data/spec/ar_mailer_aws/parse_options_spec.rb +36 -0
- data/spec/ar_mailer_aws/sender_spec.rb +158 -0
- data/spec/spec_helper.rb +29 -0
- metadata +199 -0
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
data/Gemfile
ADDED
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,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,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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|