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