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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9adfef61dff172c54ea70efa899faf4aad601259
4
- data.tar.gz: ae1a8d5225adfc1e9b405cf3cc43af0d5db2cf74
3
+ metadata.gz: 3dd90bd47a71a1620f476900cbe42fc21af4ab19
4
+ data.tar.gz: 3c607c4b6dc0c61ae76d1150dbd7c50607bb516a
5
5
  SHA512:
6
- metadata.gz: bbc40383861fe35ca24d7e64449fa07182b2308cd1ea1f031a7f0c464ee52041a565008553449f06325b9d4ce99ed24787419137f96d852cc7c27f366ca71217
7
- data.tar.gz: 2ab44794c4737b4cdce57dedcef9cd26e89306e7bcd1c63b7b72cc277fd935537a0a959ba0872bfef97d721d10785381b72819618522aaf9b5b3ad2530617271
6
+ metadata.gz: cc3555770509151eec235ef0ab845f595e5a762ca4d29646a0418dea2d2085e41709327051edb8c39df0267839e49040df55724e673c1f2b48a763f73e9fd57f
7
+ data.tar.gz: ffb2299d7e309349fc9a102213109d98fedfe5140c69a0f6f582f127fb004dd29a73b1ea25f1f949f39f86def4e78262bb4ac12ca619095cca18b5f083f1d9fb
data/.gitignore CHANGED
@@ -19,4 +19,7 @@ tmp
19
19
  .idea
20
20
  .rvmrc
21
21
  .rspec
22
+ .ruby-version
23
+ .ruby-gemset
24
+ unused
22
25
  TODO
@@ -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 butches of emails via Amazon Simple Email Service (Amazon SES) using ActiveRecord for storing messages.
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
 
@@ -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 butches of emails via Amazon SES}
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'
@@ -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 = ActiveSupport::BufferedLogger.new(Rails.root.join("log/#{Rails.env}.log"))
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
- AWS.config.logger.reopen if AWS.config.logger.respond_to?(:reopen)
41
+ ArMailerAWS.client_logger.reopen if ArMailerAWS.client_logger.respond_to?(:reopen)
42
42
 
43
43
  # run mailer
44
44
  ArMailerAWS.run(options)
@@ -1,5 +1,6 @@
1
1
  require 'ar_mailer_aws/version'
2
- require 'ar_mailer_aws/sender'
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
- sender = Sender.new(options)
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
- sender.send_batch
33
- sleep sender.options.delay
53
+ client_instance.send_batch
54
+ sleep client_instance.options.delay
34
55
  end
35
56
  end
36
57
 
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
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
- 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('-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