axtro-aws-ses 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/CHANGELOG +42 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +28 -0
- data/LICENSE +20 -0
- data/README.erb +92 -0
- data/README.rdoc +196 -0
- data/Rakefile +82 -0
- data/TODO +3 -0
- data/VERSION +1 -0
- data/aws-ses.gemspec +102 -0
- data/axtro-aws-ses.gemspec +104 -0
- data/lib/aws/actionmailer/ses_extension.rb +19 -0
- data/lib/aws/ses.rb +29 -0
- data/lib/aws/ses/addresses.rb +75 -0
- data/lib/aws/ses/base.rb +176 -0
- data/lib/aws/ses/expirable_memoize.rb +45 -0
- data/lib/aws/ses/info.rb +103 -0
- data/lib/aws/ses/response.rb +113 -0
- data/lib/aws/ses/send_email.rb +156 -0
- data/lib/aws/ses/version.rb +12 -0
- data/test/address_test.rb +72 -0
- data/test/base_test.rb +40 -0
- data/test/expirable_memoize_test.rb +120 -0
- data/test/fixtures.rb +89 -0
- data/test/helper.rb +56 -0
- data/test/info_test.rb +108 -0
- data/test/mocks/fake_response.rb +26 -0
- data/test/response_test.rb +26 -0
- data/test/send_email_test.rb +94 -0
- metadata +215 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
#:stopdoc:
|
2
|
+
module AWS
|
3
|
+
module SES
|
4
|
+
module ExpirableMemoize
|
5
|
+
module InstanceMethods
|
6
|
+
def __method__(depth = 0)
|
7
|
+
caller[depth][/`([^']+)'/, 1]
|
8
|
+
end if RUBY_VERSION <= '1.8.7'
|
9
|
+
|
10
|
+
def __called_from__
|
11
|
+
caller[1][/`([^']+)'/, 1]
|
12
|
+
end if RUBY_VERSION > '1.8.7'
|
13
|
+
|
14
|
+
def expirable_memoize(reload = false, storage = nil)
|
15
|
+
current_method = RUBY_VERSION > '1.8.7' ? __called_from__ : __method__(1)
|
16
|
+
storage = "@#{storage || current_method}"
|
17
|
+
if reload
|
18
|
+
instance_variable_set(storage, nil)
|
19
|
+
else
|
20
|
+
if cache = instance_variable_get(storage)
|
21
|
+
return cache
|
22
|
+
end
|
23
|
+
end
|
24
|
+
instance_variable_set(storage, yield)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
def memoized(method_name)
|
30
|
+
original_method = "unmemoized_#{method_name}_#{Time.now.to_i}"
|
31
|
+
alias_method original_method, method_name
|
32
|
+
module_eval(<<-EVAL, __FILE__, __LINE__)
|
33
|
+
def #{method_name}(reload = false, *args, &block)
|
34
|
+
expirable_memoize(reload) do
|
35
|
+
send(:#{original_method}, *args, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
EVAL
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#:startdoc:
|
data/lib/aws/ses/info.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
module AWS
|
2
|
+
module SES
|
3
|
+
# Adds functionality for the statistics and info send quota data that Amazon SES makes available
|
4
|
+
#
|
5
|
+
# You can access these methods as follows:
|
6
|
+
#
|
7
|
+
# ses = AWS::SES::Base.new( ... connection info ... )
|
8
|
+
#
|
9
|
+
# == Get the quota information
|
10
|
+
# response = ses.quota
|
11
|
+
# # How many e-mails you've sent in the last 24 hours
|
12
|
+
# response.sent_last_24_hours
|
13
|
+
# # How many e-mails you're allowed to send in 24 hours
|
14
|
+
# response.max_24_hour_send
|
15
|
+
# # How many e-mails you can send per second
|
16
|
+
# response.max_send_rate
|
17
|
+
#
|
18
|
+
# == Get detailed send statistics
|
19
|
+
# The result is a list of data points, representing the last two weeks of sending activity.
|
20
|
+
# Each data point in the list contains statistics for a 15-minute interval.
|
21
|
+
# GetSendStatisticsResponse#data_points is an array where each element is a hash with give string keys:
|
22
|
+
#
|
23
|
+
# * +Bounces+
|
24
|
+
# * +DeliveryAttempts+
|
25
|
+
# * +Rejects+
|
26
|
+
# * +Complaints+
|
27
|
+
# * +Timestamp+
|
28
|
+
#
|
29
|
+
# response = ses.statistics
|
30
|
+
# response.data_points # =>
|
31
|
+
# [{"Bounces"=>"0",
|
32
|
+
# "Timestamp"=>"2011-01-26T16:30:00Z",
|
33
|
+
# "DeliveryAttempts"=>"1",
|
34
|
+
# "Rejects"=>"0",
|
35
|
+
# "Complaints"=>"0"},
|
36
|
+
# {"Bounces"=>"0",
|
37
|
+
# "Timestamp"=>"2011-02-09T14:45:00Z",
|
38
|
+
# "DeliveryAttempts"=>"3",
|
39
|
+
# "Rejects"=>"0",
|
40
|
+
# "Complaints"=>"0"},
|
41
|
+
# {"Bounces"=>"0",
|
42
|
+
# "Timestamp"=>"2011-01-31T15:30:00Z",
|
43
|
+
# "DeliveryAttempts"=>"3",
|
44
|
+
# "Rejects"=>"0",
|
45
|
+
# "Complaints"=>"0"},
|
46
|
+
# {"Bounces"=>"0",
|
47
|
+
# "Timestamp"=>"2011-01-31T16:00:00Z",
|
48
|
+
# "DeliveryAttempts"=>"3",
|
49
|
+
# "Rejects"=>"0",
|
50
|
+
# "Complaints"=>"0"}]
|
51
|
+
|
52
|
+
module Info
|
53
|
+
include AWS::SES::ExpirableMemoize::InstanceMethods
|
54
|
+
extend AWS::SES::ExpirableMemoize::ClassMethods
|
55
|
+
|
56
|
+
# Returns quota information provided by SES
|
57
|
+
#
|
58
|
+
# The return format inside the response result will look like:
|
59
|
+
# {"SentLast24Hours"=>"0.0", "MaxSendRate"=>"1.0", "Max24HourSend"=>"200.0"}
|
60
|
+
def quota
|
61
|
+
request('GetSendQuota')
|
62
|
+
end
|
63
|
+
|
64
|
+
def statistics
|
65
|
+
request('GetSendStatistics')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class GetSendQuotaResponse < AWS::SES::Response
|
70
|
+
def result
|
71
|
+
parsed['GetSendQuotaResult']
|
72
|
+
end
|
73
|
+
|
74
|
+
def sent_last_24_hours
|
75
|
+
result['SentLast24Hours']
|
76
|
+
end
|
77
|
+
|
78
|
+
def max_24_hour_send
|
79
|
+
result['Max24HourSend']
|
80
|
+
end
|
81
|
+
|
82
|
+
def max_send_rate
|
83
|
+
result['MaxSendRate']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class GetSendStatisticsResponse < AWS::SES::Response
|
88
|
+
def result
|
89
|
+
if members = parsed['GetSendStatisticsResult']['SendDataPoints']
|
90
|
+
[members['member']].flatten
|
91
|
+
else
|
92
|
+
[]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
memoized :result
|
97
|
+
|
98
|
+
def data_points
|
99
|
+
result
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module AWS
|
2
|
+
module SES
|
3
|
+
class Response < String
|
4
|
+
include AWS::SES::ExpirableMemoize::InstanceMethods
|
5
|
+
extend AWS::SES::ExpirableMemoize::ClassMethods
|
6
|
+
|
7
|
+
attr_reader :response, :body, :parsed, :action
|
8
|
+
|
9
|
+
def initialize(action, response)
|
10
|
+
@action = action
|
11
|
+
@response = response
|
12
|
+
@body = response.body.to_s
|
13
|
+
super(body)
|
14
|
+
end
|
15
|
+
|
16
|
+
def headers
|
17
|
+
headers = {}
|
18
|
+
response.each do |header, value|
|
19
|
+
headers[header] = value
|
20
|
+
end
|
21
|
+
headers
|
22
|
+
end
|
23
|
+
memoized :headers
|
24
|
+
|
25
|
+
def [](header)
|
26
|
+
headers[header]
|
27
|
+
end
|
28
|
+
|
29
|
+
def each(&block)
|
30
|
+
headers.each(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def code
|
34
|
+
response.code.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
{:success => 200..299, :redirect => 300..399,
|
38
|
+
:client_error => 400..499, :server_error => 500..599}.each do |result, code_range|
|
39
|
+
class_eval(<<-EVAL, __FILE__, __LINE__)
|
40
|
+
def #{result}?
|
41
|
+
return false unless response
|
42
|
+
(#{code_range}).include? code
|
43
|
+
end
|
44
|
+
EVAL
|
45
|
+
end
|
46
|
+
|
47
|
+
def error?
|
48
|
+
!success? && (response['content-type'] == 'application/xml' || response['content-type'] == 'text/xml')
|
49
|
+
end
|
50
|
+
|
51
|
+
def error
|
52
|
+
parsed['Error']
|
53
|
+
end
|
54
|
+
memoized :error
|
55
|
+
|
56
|
+
def parsed
|
57
|
+
parse_options = { 'forcearray' => ['item', 'member'], 'suppressempty' => nil, 'keeproot' => false }
|
58
|
+
# parse_options = { 'suppressempty' => nil, 'keeproot' => false }
|
59
|
+
|
60
|
+
XmlSimple.xml_in(body, parse_options)
|
61
|
+
end
|
62
|
+
memoized :parsed
|
63
|
+
|
64
|
+
# It's expected that each subclass of Response will override this method with what part of response is relevant
|
65
|
+
def result
|
66
|
+
parsed
|
67
|
+
end
|
68
|
+
|
69
|
+
def request_id
|
70
|
+
error? ? parsed['RequestId'] : parsed['ResponseMetadata']['RequestId']
|
71
|
+
end
|
72
|
+
|
73
|
+
def inspect
|
74
|
+
"#<%s:0x%s %s %s %s>" % [self.class, object_id, request_id, response.code, response.message]
|
75
|
+
end
|
76
|
+
end # class Response
|
77
|
+
|
78
|
+
# Requests whose response code is between 300 and 599 and contain an <Error></Error> in their body
|
79
|
+
# are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception
|
80
|
+
# that corresponds to the error in the response body. The exception object contains the ErrorResponse, so
|
81
|
+
# in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and
|
82
|
+
# its Error object which contains information about the ResponseError.
|
83
|
+
#
|
84
|
+
# begin
|
85
|
+
# Bucket.create(..)
|
86
|
+
# rescue ResponseError => exception
|
87
|
+
# exception.response
|
88
|
+
# # => <Error::Response>
|
89
|
+
# exception.response.error
|
90
|
+
# # => <Error>
|
91
|
+
# end
|
92
|
+
class ResponseError < StandardError
|
93
|
+
attr_reader :response
|
94
|
+
def initialize(response)
|
95
|
+
@response = response
|
96
|
+
super("AWS::SES Response Error: #{message}")
|
97
|
+
end
|
98
|
+
|
99
|
+
def code
|
100
|
+
@response.code
|
101
|
+
end
|
102
|
+
|
103
|
+
def message
|
104
|
+
"#{@response.error['Code']} - #{@response.error['Message']}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def inspect
|
108
|
+
"#<%s:0x%s %s %s '%s'>" % [self.class.name, object_id, @response.request_id, code, message]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end # module SES
|
112
|
+
end # module AWS
|
113
|
+
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module AWS
|
2
|
+
module SES
|
3
|
+
# Adds functionality for send_email and send_raw_email
|
4
|
+
# Use the following to send an e-mail:
|
5
|
+
#
|
6
|
+
# ses = AWS::SES::Base.new( ... connection info ... )
|
7
|
+
# ses.send_email :to => ['jon@example.com', 'dave@example.com'],
|
8
|
+
# :source => '"Steve Smith" <steve@example.com>',
|
9
|
+
# :subject => 'Subject Line'
|
10
|
+
# :text_body => 'Internal text body'
|
11
|
+
#
|
12
|
+
# By default, the email "from" display address is whatever is before the @.
|
13
|
+
# To change the display from, use the format:
|
14
|
+
#
|
15
|
+
# "Steve Smith" <steve@example.com>
|
16
|
+
#
|
17
|
+
# You can also send Mail objects using send_raw_email:
|
18
|
+
#
|
19
|
+
# m = Mail.new( :to => ..., :from => ... )
|
20
|
+
# ses.send_raw_email(m)
|
21
|
+
#
|
22
|
+
# send_raw_email will also take a hash and pass it through Mail.new automatically as well.
|
23
|
+
#
|
24
|
+
module SendEmail
|
25
|
+
|
26
|
+
# Sends an email through SES
|
27
|
+
#
|
28
|
+
# the destination parameters can be:
|
29
|
+
#
|
30
|
+
# [A single e-mail string] "jon@example.com"
|
31
|
+
# [A array of e-mail addresses] ['jon@example.com', 'dave@example.com']
|
32
|
+
#
|
33
|
+
# ---
|
34
|
+
# = "Email address is not verified.MessageRejected (AWS::Error)"
|
35
|
+
# If you are receiving this message and you HAVE verified the [source] please <b>check to be sure you are not in sandbox mode!</b>
|
36
|
+
# If you have not been granted production access, you will have to <b>verify all recipients</b> as well.
|
37
|
+
# http://docs.amazonwebservices.com/ses/2010-12-01/DeveloperGuide/index.html?InitialSetup.Customer.html
|
38
|
+
# ---
|
39
|
+
#
|
40
|
+
# @option options [String] :source Source e-mail (from)
|
41
|
+
# @option options [String] :from alias for :source
|
42
|
+
# @option options [String] :to Destination e-mails
|
43
|
+
# @option options [String] :cc Destination e-mails
|
44
|
+
# @option options [String] :bcc Destination e-mails
|
45
|
+
# @option options [String] :subject
|
46
|
+
# @option options [String] :html_body
|
47
|
+
# @option options [String] :text_body
|
48
|
+
# @option options [String] :return_path The email address to which bounce notifications are to be forwarded. If the message cannot be delivered to the recipient, then an error message will be returned from the recipient's ISP; this message will then be forwarded to the email address specified by the ReturnPath parameter.
|
49
|
+
# @option options [String] :reply_to The reploy-to email address(es) for the message. If the recipient replies to the message, each reply-to address will receive the reply.
|
50
|
+
# @option options
|
51
|
+
# @return [Response] the response to sending this e-mail
|
52
|
+
def send_email(options = {})
|
53
|
+
package = {}
|
54
|
+
|
55
|
+
package['Source'] = options[:source] || options[:from]
|
56
|
+
|
57
|
+
add_array_to_hash!(package, 'Destination.ToAddresses', options[:to]) if options[:to]
|
58
|
+
add_array_to_hash!(package, 'Destination.CcAddresses', options[:cc]) if options[:cc]
|
59
|
+
add_array_to_hash!(package, 'Destination.BccAddresses', options[:bcc]) if options[:bcc]
|
60
|
+
|
61
|
+
package['Message.Subject.Data'] = options[:subject]
|
62
|
+
|
63
|
+
package['Message.Body.Html.Data'] = options[:html_body] if options[:html_body]
|
64
|
+
package['Message.Body.Text.Data'] = options[:text_body] || options[:body] if options[:text_body] || options[:body]
|
65
|
+
|
66
|
+
package['ReturnPath'] = options[:return_path] if options[:return_path]
|
67
|
+
|
68
|
+
add_array_to_hash!(package, 'ReplyToAddresses', options[:reply_to]) if options[:reply_to]
|
69
|
+
|
70
|
+
request('SendEmail', package)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Sends using the SendRawEmail method
|
74
|
+
# This gives the most control and flexibility
|
75
|
+
#
|
76
|
+
# This uses the underlying Mail object from the mail gem
|
77
|
+
# You can pass in a Mail object, a Hash of params that will be parsed by Mail.new, or just a string
|
78
|
+
#
|
79
|
+
# Note that the params are different from send_email
|
80
|
+
# Specifically, the following fields from send_email will NOT work:
|
81
|
+
#
|
82
|
+
# * :source
|
83
|
+
# * :html_body
|
84
|
+
# * :text_body
|
85
|
+
#
|
86
|
+
# send_email accepts the aliases of :from & :body in order to be more compatible with the Mail gem
|
87
|
+
#
|
88
|
+
# This method is aliased as deliver and deliver! for compatibility (especially with Rails)
|
89
|
+
#
|
90
|
+
# @option mail [String] A raw string that is a properly formatted e-mail message
|
91
|
+
# @option mail [Hash] A hash that will be parsed by Mail.new
|
92
|
+
# @option mail [Mail] A mail object, ready to be encoded
|
93
|
+
# @option args [String] :source The sender's email address
|
94
|
+
# @option args [String] :destinations A list of destinations for the message.
|
95
|
+
# @option args [String] :from alias for :source
|
96
|
+
# @option args [String] :to alias for :destinations
|
97
|
+
# @return [Response]
|
98
|
+
def send_raw_email(mail, args = {})
|
99
|
+
message = mail_to_raw_message(mail)
|
100
|
+
package = { 'RawMessage.Data' => Base64::encode64(message) }
|
101
|
+
package['Source'] = args[:from] if args[:from]
|
102
|
+
package['Source'] = args[:source] if args[:source]
|
103
|
+
if args[:destinations]
|
104
|
+
add_array_to_hash!(package, 'Destinations', args[:destinations])
|
105
|
+
else
|
106
|
+
add_array_to_hash!(package, 'Destinations', args[:to]) if args[:to]
|
107
|
+
end
|
108
|
+
request('SendRawEmail', package)
|
109
|
+
end
|
110
|
+
|
111
|
+
alias :deliver! :send_raw_email
|
112
|
+
alias :deliver :send_raw_email
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def mail_to_raw_message(mail)
|
117
|
+
if mail.is_a?(Hash)
|
118
|
+
if defined?(::Mail)
|
119
|
+
Mail.new(mail).to_s
|
120
|
+
else
|
121
|
+
raise "To use #send_raw_email with a hash, please install the Mail gem"
|
122
|
+
end
|
123
|
+
else
|
124
|
+
mail.to_s
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Adds all elements of the ary with the appropriate member elements
|
129
|
+
def add_array_to_hash!(hash, key, ary)
|
130
|
+
cnt = 1
|
131
|
+
[*ary].each do |o|
|
132
|
+
hash["#{key}.member.#{cnt}"] = o
|
133
|
+
cnt += 1
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class EmailResponse < AWS::SES::Response
|
139
|
+
def result
|
140
|
+
super["#{action}Result"]
|
141
|
+
end
|
142
|
+
|
143
|
+
def message_id
|
144
|
+
result['MessageId']
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
class SendEmailResponse < EmailResponse
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
class SendRawEmailResponse < EmailResponse
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class AddressTest < Test::Unit::TestCase
|
4
|
+
context 'verifying an address' do
|
5
|
+
setup do
|
6
|
+
@base = generate_base
|
7
|
+
end
|
8
|
+
|
9
|
+
should 'return the correct response on success' do
|
10
|
+
mock_connection(@base, :body => %{
|
11
|
+
<VerifyEmailAddressResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
12
|
+
<ResponseMetadata>
|
13
|
+
<RequestId>abc-123</RequestId>
|
14
|
+
</ResponseMetadata>
|
15
|
+
</VerifyEmailAddressResponse>
|
16
|
+
})
|
17
|
+
|
18
|
+
result = @base.addresses.verify('user1@example.com')
|
19
|
+
assert result.success?
|
20
|
+
assert_equal 'abc-123', result.request_id
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'listing verified addressess' do
|
25
|
+
setup do
|
26
|
+
@base = generate_base
|
27
|
+
end
|
28
|
+
|
29
|
+
should 'return the correct response on success' do
|
30
|
+
mock_connection(@base, :body => %{
|
31
|
+
<ListVerifiedEmailAddressesResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
32
|
+
<ListVerifiedEmailAddressesResult>
|
33
|
+
<VerifiedEmailAddresses>
|
34
|
+
<member>user1@example.com</member>
|
35
|
+
</VerifiedEmailAddresses>
|
36
|
+
</ListVerifiedEmailAddressesResult>
|
37
|
+
<ResponseMetadata>
|
38
|
+
<RequestId>abc-123</RequestId>
|
39
|
+
</ResponseMetadata>
|
40
|
+
</ListVerifiedEmailAddressesResponse>
|
41
|
+
})
|
42
|
+
|
43
|
+
result = @base.addresses.list
|
44
|
+
|
45
|
+
assert result.success?
|
46
|
+
assert_equal 'abc-123', result.request_id
|
47
|
+
assert_equal %w{user1@example.com}, result.result
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
context 'deleting a verified addressess' do
|
53
|
+
setup do
|
54
|
+
@base = generate_base
|
55
|
+
end
|
56
|
+
|
57
|
+
should 'return the correct response on success' do
|
58
|
+
mock_connection(@base, :body => %{
|
59
|
+
<DeleteVerifiedEmailAddressResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
60
|
+
<ResponseMetadata>
|
61
|
+
<RequestId>abc-123</RequestId>
|
62
|
+
</ResponseMetadata>
|
63
|
+
</DeleteVerifiedEmailAddressResponse>
|
64
|
+
})
|
65
|
+
|
66
|
+
result = @base.addresses.delete('user1@example.com')
|
67
|
+
|
68
|
+
assert result.success?
|
69
|
+
assert_equal 'abc-123', result.request_id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|