ar_mailer_aws 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +50 -0
- data/README.md +2 -1
- data/ar_mailer_aws.gemspec +5 -2
- data/bin/ar_mailer_aws +3 -3
- data/lib/ar_mailer_aws.rb +35 -87
- data/lib/ar_mailer_aws/clients/amazon_ses.rb +49 -0
- data/lib/ar_mailer_aws/clients/base.rb +117 -0
- data/lib/ar_mailer_aws/clients/mandrill.rb +53 -0
- data/lib/ar_mailer_aws/clients/smtp.rb +45 -0
- data/lib/ar_mailer_aws/options_parser.rb +107 -0
- data/lib/ar_mailer_aws/version.rb +1 -1
- data/lib/generators/ar_mailer_aws/templates/ar_mailer_aws.rb +42 -2
- data/lib/generators/ar_mailer_aws/templates/migration.rb +1 -0
- data/spec/ar_mailer_aws/ar_mailer_aws_spec.rb +42 -5
- data/spec/ar_mailer_aws/clients/amazon_ses_spec.rb +96 -0
- data/spec/ar_mailer_aws/clients/base_spec.rb +91 -0
- data/spec/ar_mailer_aws/clients/smtp_spec.rb +38 -0
- data/spec/ar_mailer_aws/mailer_spec.rb +7 -7
- data/spec/ar_mailer_aws/options_parser_spec.rb +51 -0
- data/spec/spec_helper.rb +1 -0
- metadata +74 -36
- data/lib/ar_mailer_aws/sender.rb +0 -90
- data/spec/ar_mailer_aws/parse_options_spec.rb +0 -44
- data/spec/ar_mailer_aws/sender_spec.rb +0 -158
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3dd90bd47a71a1620f476900cbe42fc21af4ab19
|
4
|
+
data.tar.gz: 3c607c4b6dc0c61ae76d1150dbd7c50607bb516a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc3555770509151eec235ef0ab845f595e5a762ca4d29646a0418dea2d2085e41709327051edb8c39df0267839e49040df55724e673c1f2b48a763f73e9fd57f
|
7
|
+
data.tar.gz: ffb2299d7e309349fc9a102213109d98fedfe5140c69a0f6f582f127fb004dd29a73b1ea25f1f949f39f86def4e78262bb4ac12ca619095cca18b5f083f1d9fb
|
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
### 0.1.0
|
2
|
+
|
3
|
+
* backwards incompatible changes
|
4
|
+
* added `send_attempts_count` column
|
5
|
+
* New configuration syntax:
|
6
|
+
```ruby
|
7
|
+
# Your system wide Amazon config
|
8
|
+
AWS.config(
|
9
|
+
:access_key_id => 'YOUR_ACCESS_KEY_ID',
|
10
|
+
:secret_access_key => 'YOUR_SECRET_ACCESS_KEY'
|
11
|
+
)
|
12
|
+
|
13
|
+
ArMailerAWS.setup do |config|
|
14
|
+
# Current delivery method
|
15
|
+
config.client = :amazon_ses
|
16
|
+
|
17
|
+
# Delivery method client log i.e. smtp, amazon, mandrill
|
18
|
+
# config.client_logger = Logger.new('path/to/log/file')
|
19
|
+
|
20
|
+
# Configure your delivery method client
|
21
|
+
config.client_config = {
|
22
|
+
# Amazon SES config, system wide config will be used if not defined
|
23
|
+
# amazon_ses: {
|
24
|
+
# access_key_id: 'YOUR_ACCESS_KEY_ID',
|
25
|
+
# secret_access_key: 'YOUR_SECRET_ACCESS_KEY',
|
26
|
+
# log_level: :debug
|
27
|
+
# #region: 'eu-west-1',
|
28
|
+
# },
|
29
|
+
|
30
|
+
# Mandrill config
|
31
|
+
# mandrill: {
|
32
|
+
# key: 'YOUR_MANDRILL_KEY'
|
33
|
+
# },
|
34
|
+
|
35
|
+
# Your smtp config, just like rails `smtp_settings`
|
36
|
+
# smtp: Rails.application.config.action_mailer.smtp_settings
|
37
|
+
}
|
38
|
+
|
39
|
+
# `ar_mailer_aws` logger i.e. mailer daemon
|
40
|
+
#config.logger = Logger.new('path/to/log/file')
|
41
|
+
|
42
|
+
# Error notification handler
|
43
|
+
#config.error_proc = lambda do |email, exception|
|
44
|
+
# ExceptionNotifier.notify_exception(exception, data: {email: email.attributes})
|
45
|
+
#end
|
46
|
+
|
47
|
+
# batch email class
|
48
|
+
# email_class = 'BatchEmail'
|
49
|
+
end
|
50
|
+
```
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[![Build Status](https://travis-ci.org/leschenko/ar_mailer_aws.png?branch=master)](https://travis-ci.org/leschenko/ar_mailer_aws)
|
4
4
|
[![Dependency Status](https://gemnasium.com/leschenko/ar_mailer_aws.png)](https://gemnasium.com/leschenko/ar_mailer_aws)
|
5
5
|
|
6
|
-
Daemon for sending
|
6
|
+
Daemon for sending batches of emails via SMTP, Amazon Simple Email Service (Amazon SES) or Mandrill using ActiveRecord for storing messages.
|
7
7
|
ArMailerAWS handles daily quotas, maximum number of emails send per second (max send rate),
|
8
8
|
batch email sending, expiring undelivered emails.
|
9
9
|
|
@@ -53,6 +53,7 @@ List available options:
|
|
53
53
|
|
54
54
|
$ bundle exec ar_mailer_aws --help
|
55
55
|
|
56
|
+
There are some configuration, see your generated `config/initializer/ar_mailer_aws.rb`
|
56
57
|
|
57
58
|
## Contributing
|
58
59
|
|
data/ar_mailer_aws.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ['Alex Leschenko']
|
10
10
|
spec.email = %w(leschenko.al@gmail.com)
|
11
11
|
spec.description = %q{Daemon for sending butches of emails via Amazon SES using ActiveRecord for storing messages. Handles daily quotas, max send rate.}
|
12
|
-
spec.summary = %q{Send
|
12
|
+
spec.summary = %q{Send batches of emails via Amazon SES}
|
13
13
|
spec.homepage = 'https://github.com/leschenko/ar_mailer_aws'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
@@ -19,9 +19,12 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = %w(lib)
|
20
20
|
|
21
21
|
spec.add_dependency 'daemons', '~> 1.1.9'
|
22
|
-
spec.add_dependency 'aws-sdk', '~> 1.0'
|
23
22
|
spec.add_dependency 'activesupport', '>= 3.0'
|
24
23
|
|
24
|
+
spec.add_development_dependency 'aws-sdk', '~> 1.0'
|
25
|
+
spec.add_development_dependency 'mandrill-api', '~> 1.0.49'
|
26
|
+
spec.add_development_dependency 'mail', '~> 2.5.4'
|
27
|
+
|
25
28
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
29
|
spec.add_development_dependency 'rake'
|
27
30
|
spec.add_development_dependency 'rspec'
|
data/bin/ar_mailer_aws
CHANGED
@@ -22,7 +22,7 @@ unless name == 'rspec'
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
options = ArMailerAWS.parse_options(ARGV)
|
25
|
+
options = ArMailerAWS::OptionsParser.parse_options(ARGV)
|
26
26
|
daemon_options = {}
|
27
27
|
daemon_options.update(dir_mode: :normal, dir: options.pid_dir) if options.pid_dir
|
28
28
|
|
@@ -30,7 +30,7 @@ Daemons.run_proc(options.app_name || 'ar_mailer_aws', daemon_options) do
|
|
30
30
|
# handle log files
|
31
31
|
if defined? Rails
|
32
32
|
ActiveRecord::Base.establish_connection
|
33
|
-
logger =
|
33
|
+
logger = Rails.logger.class.new(Rails.root.join("log/#{Rails.env}.log"))
|
34
34
|
ActiveRecord::Base.logger = logger unless Rails.env.production?
|
35
35
|
Rails.logger = ActionMailer::Base.logger = logger
|
36
36
|
end
|
@@ -38,7 +38,7 @@ Daemons.run_proc(options.app_name || 'ar_mailer_aws', daemon_options) do
|
|
38
38
|
ArMailerAWS.logger.reopen
|
39
39
|
ArMailerAWS.logger.info 'Started daemon'
|
40
40
|
end
|
41
|
-
|
41
|
+
ArMailerAWS.client_logger.reopen if ArMailerAWS.client_logger.respond_to?(:reopen)
|
42
42
|
|
43
43
|
# run mailer
|
44
44
|
ArMailerAWS.run(options)
|
data/lib/ar_mailer_aws.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'ar_mailer_aws/version'
|
2
|
-
require 'ar_mailer_aws/
|
2
|
+
require 'ar_mailer_aws/options_parser'
|
3
|
+
require 'ar_mailer_aws/clients/base'
|
3
4
|
require 'ar_mailer_aws/mailer'
|
4
5
|
require 'ar_mailer_aws/railtie' if defined? Rails
|
5
6
|
require 'active_support/core_ext'
|
@@ -10,9 +11,27 @@ module ArMailerAWS
|
|
10
11
|
mattr_accessor :email_class
|
11
12
|
@@email_class = 'BatchEmail'
|
12
13
|
|
14
|
+
# mailer client
|
15
|
+
mattr_accessor :client
|
16
|
+
|
17
|
+
# available clients
|
18
|
+
mattr_accessor :available_clients
|
19
|
+
@@available_clients = {
|
20
|
+
amazon_ses: 'ArMailerAWS::Clients::AmazonSES',
|
21
|
+
smtp: 'ArMailerAWS::Clients::SMTP',
|
22
|
+
mandrill: 'ArMailerAWS::Clients::Mandrill'
|
23
|
+
}
|
24
|
+
|
25
|
+
# mailer client credentials
|
26
|
+
mattr_accessor :client_config
|
27
|
+
@@client_config = {}
|
28
|
+
|
29
|
+
# mailer client logger
|
30
|
+
mattr_accessor :client_logger
|
31
|
+
|
32
|
+
# DEPRECATED
|
13
33
|
# options to AWS::SimpleEmailService initializer
|
14
34
|
mattr_accessor :ses_options
|
15
|
-
@@ses_options = {}
|
16
35
|
|
17
36
|
# ar_mailer_aws logger
|
18
37
|
mattr_accessor :logger
|
@@ -27,96 +46,25 @@ module ArMailerAWS
|
|
27
46
|
end
|
28
47
|
|
29
48
|
def run(options)
|
30
|
-
|
49
|
+
client_klass = find_client_klass
|
50
|
+
raise("Can not find client #{client}") unless client_klass
|
51
|
+
client_instance = client_klass.new(options)
|
31
52
|
loop do
|
32
|
-
|
33
|
-
sleep
|
53
|
+
client_instance.send_batch
|
54
|
+
sleep client_instance.options.delay
|
34
55
|
end
|
35
56
|
end
|
36
57
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
58
|
+
def find_client_klass
|
59
|
+
if client
|
60
|
+
if client.is_a?(Symbol)
|
61
|
+
available_clients[client].try(:constantize)
|
62
|
+
elsif client.is_a?(Class)
|
63
|
+
client
|
83
64
|
end
|
84
|
-
|
85
|
-
|
86
|
-
|
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('-p', '--pid-dir DIRECTORY', 'Directory for storing pid file',
|
99
|
-
'Default: Stored in current directory (named `ar_mailer_aws.pid`)') do |pid_dir|
|
100
|
-
options.pid_dir = pid_dir
|
101
|
-
end
|
102
|
-
|
103
|
-
opts.on('--app-name APP_NAME', 'Name for the daemon app',
|
104
|
-
'Default: ar_mailer_aws') do |app_name|
|
105
|
-
options.app_name = app_name
|
106
|
-
end
|
107
|
-
|
108
|
-
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
|
109
|
-
options.verbose = v
|
110
|
-
end
|
111
|
-
|
112
|
-
opts.on_tail('-h', '--help', 'Show this message') do
|
113
|
-
puts opts
|
114
|
-
exit
|
115
|
-
end
|
116
|
-
|
117
|
-
end.parse!(args)
|
118
|
-
|
119
|
-
options
|
65
|
+
else
|
66
|
+
available_clients[client_config.keys.first].try(:constantize)
|
67
|
+
end
|
120
68
|
end
|
121
69
|
|
122
70
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# too early require aws-sdk breaks mailer, need to be required by rails app
|
2
|
+
#require 'aws-sdk'
|
3
|
+
|
4
|
+
module ArMailerAWS
|
5
|
+
module Clients
|
6
|
+
class AmazonSES < Base
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
super
|
10
|
+
if ArMailerAWS.client_config[:amazon_ses].blank? && ArMailerAWS.client == :amazon_ses
|
11
|
+
ArMailerAWS.client_config[:amazon_ses] = AWS.config.to_h
|
12
|
+
end
|
13
|
+
if ArMailerAWS.ses_options && settings.blank?
|
14
|
+
ActiveSupport::Deprecation.warn('`ArMailerAWS.ses_options` is deprecated, use `ArMailerAWS.client_config[:amazon_ses]` instead')
|
15
|
+
@settings = ArMailerAWS.client_config[:amazon_ses] = ArMailerAWS.ses_options
|
16
|
+
end
|
17
|
+
@service = AWS::SimpleEmailService.new settings
|
18
|
+
end
|
19
|
+
|
20
|
+
def send_emails(emails)
|
21
|
+
emails.each do |email|
|
22
|
+
return if exceed_quota?
|
23
|
+
begin
|
24
|
+
check_rate
|
25
|
+
send_email(email)
|
26
|
+
rescue => e
|
27
|
+
handle_email_error(e, email)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def send_email(email)
|
33
|
+
log "send email to #{email.to}"
|
34
|
+
@service.send_raw_email email.mail, from: email.from, to: email.to
|
35
|
+
email.destroy
|
36
|
+
@sent_count += 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def sent_last_24_hours
|
40
|
+
@sent_last_24_hours ||= begin
|
41
|
+
count = @service.quotas[:sent_last_24_hours]
|
42
|
+
log "#{count} emails sent last 24 hours"
|
43
|
+
count
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module ArMailerAWS
|
2
|
+
module Clients
|
3
|
+
|
4
|
+
autoload :SMTP, 'ar_mailer_aws/clients/smtp'
|
5
|
+
autoload :AmazonSES, 'ar_mailer_aws/clients/amazon_ses'
|
6
|
+
autoload :Mandrill, 'ar_mailer_aws/clients/mandrill'
|
7
|
+
|
8
|
+
class Base
|
9
|
+
attr_reader :options, :model, :service
|
10
|
+
|
11
|
+
def initialize(options={})
|
12
|
+
@options = options.is_a?(Hash) ? OpenStruct.new(options) : options
|
13
|
+
@model = ArMailerAWS.email_class.constantize
|
14
|
+
@day = Date.today
|
15
|
+
@sent_count = 0
|
16
|
+
@sent_per_second = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def settings
|
20
|
+
@settings ||= begin
|
21
|
+
config_key = self.class.name.split('::').last.underscore.to_sym
|
22
|
+
ArMailerAWS.client_config[config_key] or raise("Provide setting via `ArMailerAWS.client_config[:#{config_key}]`")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def send_batch
|
27
|
+
cleanup
|
28
|
+
emails = find_emails
|
29
|
+
log "found #{emails.length} emails to deliver"
|
30
|
+
send_emails(emails) unless emails.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def send_emails(emails)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_emails
|
38
|
+
@model.where('last_send_attempt_at IS NULL OR last_send_attempt_at < ?', Time.now - 300).limit(options.batch_size)
|
39
|
+
end
|
40
|
+
|
41
|
+
def cleanup
|
42
|
+
max_age = options.max_age.to_i
|
43
|
+
max_attempts = options.max_attempts.to_i
|
44
|
+
return if max_age.zero? && max_attempts.zero?
|
45
|
+
|
46
|
+
scope = @model
|
47
|
+
scope = scope.where('last_send_attempt_at IS NOT NULL AND created_at < ?', Time.now - max_age) unless max_age.zero?
|
48
|
+
scope = scope.where('send_attempts_count > ?', max_attempts) unless max_attempts.zero?
|
49
|
+
|
50
|
+
log "expired #{scope.destroy_all.length} emails"
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def check_rate
|
56
|
+
if @sent_per_second == options.rate
|
57
|
+
sleep 1
|
58
|
+
@sent_per_second = 0
|
59
|
+
else
|
60
|
+
@sent_per_second += 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def handle_email_error(e, email, options={})
|
65
|
+
log "ERROR sending email #{email.id} - #{email.inspect}: #{e.message}\n #{e.backtrace.join("\n ")}", :error
|
66
|
+
ArMailerAWS.error_proc.call(email, e) if ArMailerAWS.error_proc
|
67
|
+
email.increment!(:send_attempts_count) if options[:email_error]
|
68
|
+
email.update_column(:last_send_attempt_at, Time.now)
|
69
|
+
end
|
70
|
+
|
71
|
+
def exceed_quota?
|
72
|
+
return false unless options.quota
|
73
|
+
if @day == Date.today
|
74
|
+
is_exceed_quota = options.quota <= @sent_count + sent_last_24_hours
|
75
|
+
log("exceed daily quota in #{@quota}, sent #{@sent_count} (total #{@sent_last_24_hours})") if is_exceed_quota
|
76
|
+
is_exceed_quota
|
77
|
+
else
|
78
|
+
@sent_count = 0
|
79
|
+
@sent_last_24_hours = nil
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def sent_last_24_hours
|
85
|
+
0
|
86
|
+
end
|
87
|
+
|
88
|
+
def log(msg, level=:info)
|
89
|
+
formatted_msg = "[#{Time.now}] batch_mailer: #{msg}"
|
90
|
+
puts formatted_msg if options.verbose
|
91
|
+
if logger
|
92
|
+
logger.send(level, msg)
|
93
|
+
elsif options.verbose && Object.const_defined?('Rails')
|
94
|
+
Rails.logger.send(level, formatted_msg)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def logger
|
99
|
+
ArMailerAWS.logger
|
100
|
+
end
|
101
|
+
|
102
|
+
def client_log(msg, level=:info)
|
103
|
+
formatted_msg = "[#{Time.now}] batch_mailer_client: #{msg}"
|
104
|
+
puts formatted_msg if options.verbose
|
105
|
+
if client_logger
|
106
|
+
client_logger.send(level, msg)
|
107
|
+
elsif options.verbose && Object.const_defined?('Rails')
|
108
|
+
Rails.logger.send(level, formatted_msg)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def client_logger
|
113
|
+
ArMailerAWS.client_logger
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|