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.
@@ -0,0 +1,53 @@
1
+ require 'mandrill'
2
+ require 'mandrill/api'
3
+ require 'mail'
4
+
5
+ module ArMailerAWS
6
+ module Clients
7
+ class Mandrill < Base
8
+
9
+ REJECT_HEADERS = ['From', 'To', 'Subject']
10
+
11
+ def initialize(options={})
12
+ super
13
+ @service = ::Mandrill::API.new settings[:key]
14
+ end
15
+
16
+ def send_emails(emails)
17
+ emails.each do |email|
18
+ return if exceed_quota?
19
+ begin
20
+ check_rate
21
+ send_email(email)
22
+ rescue => e
23
+ handle_email_error(e, email)
24
+ end
25
+ end
26
+ end
27
+
28
+ def send_email(email)
29
+ log "send email to #{email.to}"
30
+ email_json_hash = email_json(email)
31
+ client_log email_json_hash, :debug
32
+ resp = @service.messages.send email_json_hash
33
+ client_log resp, :debug
34
+ email.destroy
35
+ @sent_count += 1
36
+ end
37
+
38
+ def email_json(email)
39
+ mail = Mail.new(email.mail)
40
+ headers = mail.header.reject{|h| REJECT_HEADERS.include?(h.name) }.map { |h| [h.name, h.value] }.to_hash
41
+ {
42
+ 'subject' => mail.subject.to_s.force_encoding('UTF-8'),
43
+ 'html' => mail.body.to_s.force_encoding('UTF-8'),
44
+ 'headers' => headers,
45
+ 'from_email' => email.from,
46
+ 'track_opens' => false,
47
+ 'track_clicks' => false,
48
+ 'to' => [{'email' => email.to, 'type' => 'to'}]
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ require 'net/smtp'
2
+
3
+ module ArMailerAWS
4
+ module Clients
5
+ class SMTP < Base
6
+
7
+ def send_emails(emails)
8
+ session = Net::SMTP.new(settings[:address], settings[:port])
9
+ session.enable_starttls_auto if settings[:enable_starttls_auto]
10
+ begin
11
+ session.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication]) do |smtp|
12
+ emails.each do |email|
13
+ begin
14
+ break if exceed_quota?
15
+ send_email(smtp, email)
16
+ rescue Net::SMTPFatalError => e
17
+ handle_email_error(e, email, email_error: true)
18
+ session.reset
19
+ rescue Net::SMTPServerBusy
20
+ log 'server too busy, stopping delivery cycle'
21
+ return
22
+ rescue Net::SMTPUnknownError, Net::SMTPSyntaxError, TimeoutError, Timeout::Error => e
23
+ handle_email_error(e, email, email_error: true)
24
+ session.reset
25
+ rescue => e
26
+ handle_email_error(e, email)
27
+ end
28
+ end
29
+ end
30
+ rescue => e
31
+ log "ERROR in SMTP session: #{e.message}\n #{e.backtrace.join("\n ")}", :error
32
+ ArMailerAWS.error_proc.call(nil, e) if ArMailerAWS.error_proc
33
+ end
34
+ end
35
+
36
+ def send_email(smtp, email)
37
+ log "send email to #{email.to}"
38
+ client_log smtp.send_message(email.mail, email.from, email.to)
39
+ email.destroy
40
+ @sent_count += 1
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,107 @@
1
+ module ArMailerAWS
2
+ class OptionsParser
3
+
4
+ def self.parse_options(all_args)
5
+ start_i = all_args.index('--').try(:succ) || 0
6
+ args = all_args[start_i..-1]
7
+ new(args).parse
8
+ end
9
+
10
+ def initialize(args)
11
+ @args = args
12
+ @options = OpenStruct.new
13
+ end
14
+
15
+ def parse
16
+ option_parser.parse!(@args)
17
+ @options
18
+ end
19
+
20
+ def option_parser
21
+ OptionParser.new do |opts|
22
+ @options.batch_size = 100
23
+ @options.delay = 180
24
+ @options.quota = 10_000
25
+ @options.rate = 5
26
+ @options.max_age = 3600 * 24 * 7
27
+ @options.max_attempts = 5
28
+
29
+ opts.banner = <<-TXT.strip_heredoc
30
+ Usage: ar_mailer_aws <command> <options> -- <application options>
31
+
32
+ * where <command> is one of:
33
+ start start an instance of the application
34
+ stop stop all instances of the application
35
+ restart stop all instances and restart them afterwards
36
+ reload send a SIGHUP to all instances of the application
37
+ run start the application and stay on top
38
+ zap set the application to a stopped state
39
+ status show status (PID) of application instances
40
+
41
+ * where <options> may contain several of the following:
42
+
43
+ -t, --ontop Stay on top (does not daemonize)
44
+ -f, --force Force operation
45
+ -n, --no_wait Do not wait for processes to stop
46
+
47
+ * and where <application options> may contain several of the following:
48
+
49
+ TXT
50
+
51
+ opts.on('-b', '--batch-size BATCH_SIZE', 'Maximum number of emails to send per delay',
52
+ "Default: #{@options.batch_size}", Integer) do |batch_size|
53
+ @options.batch_size = batch_size
54
+ end
55
+
56
+ opts.on('-d', '--delay DELAY', 'Delay between checks for new mail in the database',
57
+ "Default: #{@options.delay}", Integer) do |delay|
58
+ @options.delay = delay
59
+ end
60
+
61
+ opts.on('-q', '--quota QUOTA', 'Daily quota for sending emails', "Default: #{@options.quota}", Integer) do |quota|
62
+ @options.quota = quota
63
+ end
64
+
65
+ opts.on('-r', '--rate RATE', 'Maximum number of emails send per second',
66
+ "Default: #{@options.rate}", Integer) do |rate|
67
+ @options.rate = rate
68
+ end
69
+
70
+ opts.on('-a', '--max-attempts MAX_ATTEMPTS',
71
+ 'Maximum attempts count for an email.',
72
+ 'After this it will be removed from the queue.',
73
+ 'Set to 0 to disable queue cleanup.',
74
+ "Default: #{@options.max_attempts}", Integer) do |max_attempts|
75
+ @options.max_attempts = max_attempts
76
+ end
77
+
78
+ opts.on('-m', '--max-age MAX_AGE',
79
+ 'Maximum age for an email. After this',
80
+ 'it will be removed from the queue.',
81
+ 'Set to 0 to disable queue cleanup.',
82
+ "Default: #{@options.max_age} seconds", Integer) do |max_age|
83
+ @options.max_age = max_age
84
+ end
85
+
86
+ opts.on('-p', '--pid-dir DIRECTORY', 'Directory for storing pid file',
87
+ 'Default: Stored in current directory (named `ar_mailer_aws.pid`)') do |pid_dir|
88
+ @options.pid_dir = pid_dir
89
+ end
90
+
91
+ opts.on('--app-name APP_NAME', 'Name for the daemon app',
92
+ 'Default: ar_mailer_aws') do |app_name|
93
+ @options.app_name = app_name
94
+ end
95
+
96
+ opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
97
+ @options.verbose = v
98
+ end
99
+
100
+ opts.on_tail('-h', '--help', 'Show this message') do
101
+ puts opts
102
+ exit
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -1,3 +1,3 @@
1
1
  module ArMailerAWS
2
- VERSION = '0.0.4'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -1,10 +1,50 @@
1
+ # Your system wide Amazon config
1
2
  AWS.config(
2
3
  :access_key_id => 'YOUR_ACCESS_KEY_ID',
3
4
  :secret_access_key => 'YOUR_SECRET_ACCESS_KEY'
4
5
  )
5
6
 
7
+ ArMailerAWS.setup do |config|
8
+ # Current delivery method
9
+ config.client = :amazon_ses
10
+
11
+ # Delivery method client log i.e. smtp, amazon, mandrill
12
+ # config.client_logger = Logger.new('path/to/log/file')
13
+
14
+ # Configure your delivery method client
15
+ config.client_config = {
16
+ # Amazon SES config, system wide config will be used if not defined
17
+ # amazon_ses: {
18
+ # access_key_id: 'YOUR_ACCESS_KEY_ID',
19
+ # secret_access_key: 'YOUR_SECRET_ACCESS_KEY',
20
+ # log_level: :debug
21
+ # #region: 'eu-west-1',
22
+ # },
23
+
24
+ # Mandrill config
25
+ # mandrill: {
26
+ # key: 'YOUR_MANDRILL_KEY'
27
+ # },
28
+
29
+ # Your smtp config, just like rails `smtp_settings`
30
+ # smtp: Rails.application.config.action_mailer.smtp_settings
31
+ }
32
+
33
+ # `ar_mailer_aws` logger i.e. mailer daemon
34
+ #config.logger = Logger.new('path/to/log/file')
35
+
36
+ # Error notification handler
37
+ #config.error_proc = lambda do |email, exception|
38
+ # ExceptionNotifier.notify_exception(exception, data: {email: email.try(:attributes)})
39
+ #end
40
+
41
+ # batch email class
6
42
  <% if class_name != 'BatchEmail' -%>
7
- ArMailerAWS.email_class = '<%= class_name %>'
43
+ email_class = '<%= class_name %>'
44
+ <% else -%>
45
+ # email_class = 'BatchEmail'
8
46
  <% end -%>
47
+ end
9
48
 
10
- #ActionMailer::Base.delivery_method = :ar_mailer_aws
49
+ # Set as default delivery method
50
+ # ActionMailer::Base.delivery_method = :ar_mailer_aws
@@ -4,6 +4,7 @@ class <%= migration_class_name.gsub(/::/, '') %> < ActiveRecord::Migration
4
4
  t.string :from
5
5
  t.string :to
6
6
  t.text :mail, limit: 16777215
7
+ t.integer :send_attempts_count, default: 0
7
8
  t.datetime :last_send_attempt_at
8
9
  t.datetime :created_at
9
10
  end
@@ -4,18 +4,19 @@ describe ArMailerAWS do
4
4
 
5
5
  it 'setup yields ArMailerAWS' do
6
6
  ArMailerAWS.setup do |config|
7
- config.should == ArMailerAWS
7
+ expect(config).to eq ArMailerAWS
8
8
  end
9
9
  end
10
10
 
11
- describe '#run', focus: true do
11
+ describe '#run' do
12
12
  before do
13
- @sender = ArMailerAWS::Sender.new(delay: 1)
14
- ArMailerAWS::Sender.stub(:new).and_return(@sender)
13
+ allow(ArMailerAWS).to receive(:client_config).and_return({amazon_ses: {}})
14
+ @client = ArMailerAWS::Clients::AmazonSES.new(delay: 1)
15
+ allow(ArMailerAWS::Clients::AmazonSES).to receive(:new).and_return(@client)
15
16
  end
16
17
 
17
18
  it 'run sender' do
18
- @sender.should_receive(:send_batch).twice
19
+ expect(@client).to receive(:send_batch).twice
19
20
  begin
20
21
  Timeout::timeout(1.5) do
21
22
  ArMailerAWS.run({})
@@ -23,6 +24,42 @@ describe ArMailerAWS do
23
24
  rescue Timeout::Error
24
25
  end
25
26
  end
27
+ end
28
+
29
+ describe '#find_client_klass' do
30
+ context 'option as Symbol' do
31
+ it 'resolve symbol to class' do
32
+ allow(ArMailerAWS).to receive(:client).and_return(:smtp)
33
+ expect(ArMailerAWS.find_client_klass).to eq ArMailerAWS::Clients::SMTP
34
+ end
35
+
36
+ it 'resolve symbol to class 2' do
37
+ allow(ArMailerAWS).to receive(:client).and_return(:amazon_ses)
38
+ expect(ArMailerAWS.find_client_klass).to eq ArMailerAWS::Clients::AmazonSES
39
+ end
40
+ end
41
+
42
+ context 'option as Class' do
43
+ before do
44
+ allow(ArMailerAWS).to receive(:client).and_return(Object)
45
+ end
26
46
 
47
+ it 'return direct value' do
48
+ expect(ArMailerAWS.find_client_klass).to eq Object
49
+ end
50
+ end
51
+
52
+ context 'resolve from client_config' do
53
+ it 'resolve symbol to class' do
54
+ allow(ArMailerAWS).to receive(:client_config).and_return({smtp: {}})
55
+ expect(ArMailerAWS.find_client_klass).to eq ArMailerAWS::Clients::SMTP
56
+ end
57
+
58
+ it 'resolve symbol to class 2' do
59
+ allow(ArMailerAWS).to receive(:client_config).and_return({amazon_ses: {}})
60
+ expect(ArMailerAWS.find_client_klass).to eq ArMailerAWS::Clients::AmazonSES
61
+ end
62
+ end
27
63
  end
64
+
28
65
  end
@@ -0,0 +1,96 @@
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::Clients::AmazonSES do
8
+
9
+ it 'supply ses options to AWS::SimpleEmailService initializer' do
10
+ ArMailerAWS.client_config = {amazon_ses: {a: 1}}
11
+ expect(AWS::SimpleEmailService).to receive(:new).with({a: 1})
12
+ ArMailerAWS::Clients::AmazonSES.new
13
+ end
14
+
15
+ context 'sending' do
16
+ before do
17
+ BatchEmail.delete_all
18
+ end
19
+
20
+ describe '#send_emails' do
21
+ before do
22
+ @client = ArMailerAWS::Clients::AmazonSES.new(quota: 100)
23
+ allow(@client.service).to receive(:send_raw_email)
24
+ allow(@client.service).to receive(:quotas).and_return({sent_last_24_hours: 0})
25
+ end
26
+
27
+ context 'success' do
28
+ it 'send email via ses' do
29
+ 2.times { create_email }
30
+ expect(@client.service).to receive(:send_raw_email).twice
31
+ @client.send_emails(@client.model.all)
32
+ end
33
+
34
+ it 'remove sent emails' do
35
+ 2.times { create_email }
36
+ expect {
37
+ @client.send_emails(@client.model.all)
38
+ }.to change { @client.model.count }.from(2).to(0)
39
+ end
40
+ end
41
+
42
+ context 'error' do
43
+ it 'call error_proc' do
44
+ email = create_email
45
+ exception = StandardError.new
46
+ ArMailerAWS.error_proc = proc {}
47
+ expect(ArMailerAWS.error_proc).to receive(:call).with(email, exception)
48
+ expect(@client.service).to receive(:send_raw_email).and_raise(exception)
49
+ @client.send_emails([email])
50
+ end
51
+
52
+ it 'update last_send_attempt_at column' do
53
+ email = create_email
54
+ exception = StandardError.new
55
+ expect(@client.service).to receive(:send_raw_email).and_raise(exception)
56
+ @client.send_emails([email])
57
+ expect(email.reload.last_send_attempt_at).not_to be_nil
58
+ end
59
+ end
60
+
61
+ context 'rate' do
62
+ it 'call not more the rate times per second' do
63
+ 5.times { create_email }
64
+ @client.options.rate = 2
65
+ expect(@client.service).to receive(:send_raw_email).twice
66
+ begin
67
+ Timeout::timeout(1) do
68
+ @client.send_emails(@client.model.all)
69
+ end
70
+ rescue Timeout::Error
71
+ end
72
+ end
73
+ end
74
+
75
+ context 'quota' do
76
+ it 'not exceed quota' do
77
+ 10.times { create_email }
78
+ @client.options.quota = 5
79
+ expect {
80
+ @client.send_emails(@client.model.all)
81
+ }.to change { @client.model.count }.by(-5)
82
+ end
83
+
84
+ it 'consider sent_last_24_hours from ses' do
85
+ 10.times { create_email }
86
+ allow(@client.service).to receive(:quotas).and_return({sent_last_24_hours: 10})
87
+ @client.options.quota = 15
88
+ expect {
89
+ @client.send_emails(@client.model.all)
90
+ }.to change { @client.model.count }.by(-5)
91
+ end
92
+ end
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,91 @@
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::Clients::Base do
8
+
9
+ it 'convert Hash options to OpenStruct' do
10
+ client = ArMailerAWS::Clients::Base.new
11
+ expect(client.options.class.name).to eq 'OpenStruct'
12
+ end
13
+
14
+ it 'get default emails model' do
15
+ expect(ArMailerAWS::Clients::Base.new.model.name).to eq 'BatchEmail'
16
+ end
17
+
18
+ context 'sending' do
19
+ before do
20
+ BatchEmail.delete_all
21
+ end
22
+
23
+ describe '#find_emails' do
24
+ it 'batch_size emails' do
25
+ 5.times { create_email }
26
+ @client = ArMailerAWS::Clients::Base.new(batch_size: 3)
27
+ expect(@client.find_emails.length).to eq 3
28
+ end
29
+
30
+ it 'ignore emails last_send_attempt_at < 300 seconds ago' do
31
+ 2.times { create_email }
32
+ 2.times { create_email(last_send_attempt_at: Time.now - 100) }
33
+ @client = ArMailerAWS::Clients::Base.new(batch_size: 3)
34
+ expect(@client.find_emails.length).to eq 2
35
+ end
36
+ end
37
+
38
+ describe '#cleanup' do
39
+ it 'do nothing if max_age zero and max_attempts zero' do
40
+ @client = ArMailerAWS::Clients::Base.new(max_age: 0, max_attempts: 0)
41
+ expect(@client.model).not_to receive(:where)
42
+ @client.cleanup
43
+ end
44
+
45
+ it 'remove emails with last_send_attempt_at and create_at greater then max_age' do
46
+ 2.times { create_email }
47
+ 2.times { create_email(last_send_attempt_at: Time.now, created_at: Time.now - 4000) }
48
+
49
+ @client = ArMailerAWS::Clients::Base.new(max_age: 3600)
50
+ expect {
51
+ @client.cleanup
52
+ }.to change { @client.model.count }.from(4).to(2)
53
+ end
54
+
55
+ it 'remove emails with send_attempts_count greater then max_attempts' do
56
+ 2.times { create_email }
57
+ 2.times { create_email(send_attempts_count: 11) }
58
+
59
+ @client = ArMailerAWS::Clients::Base.new(max_attempts: 10)
60
+ expect { @client.cleanup }.to change { @client.model.count }.from(4).to(2)
61
+ end
62
+ end
63
+
64
+ describe '#send_emails' do
65
+ it 'raise not implemented error' do
66
+ expect{ ArMailerAWS::Clients::Base.new.send_emails([]) }.to raise_error(NotImplementedError)
67
+ end
68
+ end
69
+
70
+ describe '#send_batch' do
71
+ before do
72
+ @client = ArMailerAWS::Clients::Base.new
73
+ allow(@client).to receive(:send_emails)
74
+ end
75
+
76
+ it 'no pending emails' do
77
+ expect(@client).to receive(:cleanup)
78
+ expect(@client).to receive(:find_emails).and_return([])
79
+ expect(@client).not_to receive(:send_emails)
80
+ @client.send_batch
81
+ end
82
+
83
+ it 'no pending emails' do
84
+ expect(@client).to receive(:find_emails).and_return([create_email])
85
+ expect(@client).to receive(:send_emails)
86
+ @client.send_batch
87
+ end
88
+ end
89
+
90
+ end
91
+ end