ar_mailer_aws 0.0.4 → 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.
- 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
|
[](https://travis-ci.org/leschenko/ar_mailer_aws)
|
4
4
|
[](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
|