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 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