aws-ses-v4 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ #:stopdoc:
2
+ module Kernel
3
+ def __method__(depth = 0)
4
+ caller[depth][/`([^']+)'/, 1]
5
+ end if RUBY_VERSION <= '1.8.7'
6
+
7
+ def __called_from__
8
+ caller[1][/`([^']+)'/, 1]
9
+ end if RUBY_VERSION > '1.8.7'
10
+
11
+ def expirable_memoize(reload = false, storage = nil)
12
+ current_method = RUBY_VERSION > '1.8.7' ? __called_from__ : __method__(1)
13
+ storage = "@#{storage || current_method}"
14
+ if reload
15
+ instance_variable_set(storage, nil)
16
+ else
17
+ if cache = instance_variable_get(storage)
18
+ return cache
19
+ end
20
+ end
21
+ instance_variable_set(storage, yield)
22
+ end
23
+ end
24
+
25
+ class Module
26
+ def memoized(method_name)
27
+ original_method = "unmemoized_#{method_name}_#{Time.now.to_i}"
28
+ alias_method original_method, method_name
29
+ module_eval(<<-EVAL, __FILE__, __LINE__)
30
+ def #{method_name}(reload = false, *args, &block)
31
+ expirable_memoize(reload) do
32
+ send(:#{original_method}, *args, &block)
33
+ end
34
+ end
35
+ EVAL
36
+ end
37
+ end
38
+
39
+ #:startdoc:
@@ -0,0 +1,100 @@
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
+ # Returns quota information provided by SES
54
+ #
55
+ # The return format inside the response result will look like:
56
+ # {"SentLast24Hours"=>"0.0", "MaxSendRate"=>"1.0", "Max24HourSend"=>"200.0"}
57
+ def quota
58
+ request('GetSendQuota')
59
+ end
60
+
61
+ def statistics
62
+ request('GetSendStatistics')
63
+ end
64
+ end
65
+
66
+ class GetSendQuotaResponse < AWS::SES::Response
67
+ def result
68
+ parsed['GetSendQuotaResult']
69
+ end
70
+
71
+ def sent_last_24_hours
72
+ result['SentLast24Hours']
73
+ end
74
+
75
+ def max_24_hour_send
76
+ result['Max24HourSend']
77
+ end
78
+
79
+ def max_send_rate
80
+ result['MaxSendRate']
81
+ end
82
+ end
83
+
84
+ class GetSendStatisticsResponse < AWS::SES::Response
85
+ def result
86
+ if members = parsed['GetSendStatisticsResult']['SendDataPoints']
87
+ [members['member']].flatten
88
+ else
89
+ []
90
+ end
91
+ end
92
+
93
+ memoized :result
94
+
95
+ def data_points
96
+ result
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,110 @@
1
+ module AWS
2
+ module SES
3
+ class Response < String
4
+ attr_reader :response, :body, :parsed, :action
5
+
6
+ def initialize(action, response)
7
+ @action = action
8
+ @response = response
9
+ @body = response.body.to_s
10
+ super(body)
11
+ end
12
+
13
+ def headers
14
+ headers = {}
15
+ response.each do |header, value|
16
+ headers[header] = value
17
+ end
18
+ headers
19
+ end
20
+ memoized :headers
21
+
22
+ def [](header)
23
+ headers[header]
24
+ end
25
+
26
+ def each(&block)
27
+ headers.each(&block)
28
+ end
29
+
30
+ def code
31
+ response.code.to_i
32
+ end
33
+
34
+ {:success => 200..299, :redirect => 300..399,
35
+ :client_error => 400..499, :server_error => 500..599}.each do |result, code_range|
36
+ class_eval(<<-EVAL, __FILE__, __LINE__)
37
+ def #{result}?
38
+ return false unless response
39
+ (#{code_range}).include? code
40
+ end
41
+ EVAL
42
+ end
43
+
44
+ def error?
45
+ !success? && (response['content-type'] == 'application/xml' || response['content-type'] == 'text/xml')
46
+ end
47
+
48
+ def error
49
+ parsed['Error']
50
+ end
51
+ memoized :error
52
+
53
+ def parsed
54
+ parse_options = { 'forcearray' => ['item', 'member'], 'suppressempty' => nil, 'keeproot' => false }
55
+ # parse_options = { 'suppressempty' => nil, 'keeproot' => false }
56
+
57
+ XmlSimple.xml_in(body, parse_options)
58
+ end
59
+ memoized :parsed
60
+
61
+ # It's expected that each subclass of Response will override this method with what part of response is relevant
62
+ def result
63
+ parsed
64
+ end
65
+
66
+ def request_id
67
+ error? ? parsed['RequestId'] : parsed['ResponseMetadata']['RequestId']
68
+ end
69
+
70
+ def inspect
71
+ "#<%s:0x%s %s %s %s>" % [self.class, object_id, request_id, response.code, response.message]
72
+ end
73
+ end # class Response
74
+
75
+ # Requests whose response code is between 300 and 599 and contain an <Error></Error> in their body
76
+ # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception
77
+ # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so
78
+ # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and
79
+ # its Error object which contains information about the ResponseError.
80
+ #
81
+ # begin
82
+ # Bucket.create(..)
83
+ # rescue ResponseError => exception
84
+ # exception.response
85
+ # # => <Error::Response>
86
+ # exception.response.error
87
+ # # => <Error>
88
+ # end
89
+ class ResponseError < StandardError
90
+ attr_reader :response
91
+ def initialize(response)
92
+ @response = response
93
+ super("AWS::SES Response Error: #{message}")
94
+ end
95
+
96
+ def code
97
+ @response.code
98
+ end
99
+
100
+ def message
101
+ "#{@response.error['Code']} - #{@response.error['Message']}"
102
+ end
103
+
104
+ def inspect
105
+ "#<%s:0x%s %s %s '%s'>" % [self.class.name, object_id, @response.request_id, code, message]
106
+ end
107
+ end
108
+ end # module SES
109
+ end # module AWS
110
+
@@ -0,0 +1,159 @@
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.is_a?(Hash) ? Mail.new(mail) : mail
100
+ raise ArgumentError, "Attachment provided without message body" if message.has_attachments? && message.text_part.nil? && message.html_part.nil?
101
+
102
+ raw_email = build_raw_email(message, args)
103
+ result = request('SendRawEmail', raw_email)
104
+ message.message_id = "<#{result.parsed['SendRawEmailResult']['MessageId']}@#{message_id_domain}>"
105
+ result
106
+ end
107
+
108
+ alias :deliver! :send_raw_email
109
+ alias :deliver :send_raw_email
110
+
111
+ private
112
+
113
+ def build_raw_email(message, args = {})
114
+ # the message.to_s includes the :to and :cc addresses
115
+ package = { 'RawMessage.Data' => Base64::encode64(message.to_s) }
116
+ package['Source'] = args[:from] if args[:from]
117
+ package['Source'] = args[:source] if args[:source]
118
+ if args[:destinations]
119
+ add_array_to_hash!(package, 'Destinations', args[:destinations])
120
+ else
121
+ mail_addresses = [message.to, message.cc, message.bcc].flatten.compact
122
+ args_addresses = [args[:to], args[:cc], args[:bcc]].flatten.compact
123
+
124
+ mail_addresses = args_addresses unless args_addresses.empty?
125
+
126
+ add_array_to_hash!(package, 'Destinations', mail_addresses)
127
+ end
128
+ package
129
+ end
130
+
131
+ # Adds all elements of the ary with the appropriate member elements
132
+ def add_array_to_hash!(hash, key, ary)
133
+ cnt = 1
134
+ [*ary].each do |o|
135
+ hash["#{key}.member.#{cnt}"] = o
136
+ cnt += 1
137
+ end
138
+ end
139
+ end
140
+
141
+ class EmailResponse < AWS::SES::Response
142
+ def result
143
+ super["#{action}Result"]
144
+ end
145
+
146
+ def message_id
147
+ result['MessageId']
148
+ end
149
+ end
150
+
151
+ class SendEmailResponse < EmailResponse
152
+
153
+ end
154
+
155
+ class SendRawEmailResponse < EmailResponse
156
+
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,12 @@
1
+ module AWS
2
+ module SES
3
+ module VERSION #:nodoc:
4
+ MAJOR = '0'
5
+ MINOR = '4'
6
+ TINY = '4'
7
+ BETA = Time.now.to_i.to_s
8
+ end
9
+
10
+ Version = [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY, VERSION::BETA].compact * '.'
11
+ end
12
+ 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