aws-ses 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.
- data/.document +5 -0
- data/CHANGELOG +6 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +28 -0
- data/LICENSE +20 -0
- data/README +47 -0
- data/README.erb +25 -0
- data/Rakefile +85 -0
- data/TODO +2 -0
- data/VERSION +1 -0
- data/lib/aws/ses.rb +22 -0
- data/lib/aws/ses/addresses.rb +61 -0
- data/lib/aws/ses/base.rb +213 -0
- data/lib/aws/ses/exceptions.rb +50 -0
- data/lib/aws/ses/extensions.rb +313 -0
- data/lib/aws/ses/response.rb +97 -0
- data/lib/aws/ses/version.rb +12 -0
- data/test/base_test.rb +9 -0
- data/test/extensions_test.rb +340 -0
- data/test/fixtures.rb +89 -0
- data/test/helper.rb +48 -0
- data/test/mocks/fake_response.rb +26 -0
- data/test/response_test.rb +33 -0
- metadata +257 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
#--
|
2
|
+
# AWS ERROR CODES
|
3
|
+
# AWS can throw error exceptions that contain a '.' in them.
|
4
|
+
# since we can't name an exception class with that '.' I compressed
|
5
|
+
# each class name into the non-dot version which allows us to retain
|
6
|
+
# the granularity of the exception.
|
7
|
+
#++
|
8
|
+
|
9
|
+
module AWS
|
10
|
+
|
11
|
+
# All AWS errors are superclassed by Error < RuntimeError
|
12
|
+
class Error < RuntimeError; end
|
13
|
+
|
14
|
+
# CLIENT : A client side argument error
|
15
|
+
class ArgumentError < Error; end
|
16
|
+
|
17
|
+
# Server Error Codes
|
18
|
+
###
|
19
|
+
|
20
|
+
# Server : Internal Error.
|
21
|
+
class InternalError < Error; end
|
22
|
+
|
23
|
+
# Server : Not enough available addresses to satisfy your minimum request.
|
24
|
+
class InsufficientAddressCapacity < Error; end
|
25
|
+
|
26
|
+
# Server : There are not enough available instances to satisfy your minimum request.
|
27
|
+
class InsufficientInstanceCapacity < Error; end
|
28
|
+
|
29
|
+
# Server : There are not enough available reserved instances to satisfy your minimum request.
|
30
|
+
class InsufficientReservedInstanceCapacity < Error; end
|
31
|
+
|
32
|
+
# Server : The server is overloaded and cannot handle the request.
|
33
|
+
class Unavailable < Error; end
|
34
|
+
|
35
|
+
# API Errors
|
36
|
+
############################
|
37
|
+
|
38
|
+
# Server : Invalid AWS Account
|
39
|
+
class InvalidClientTokenId < Error; end
|
40
|
+
|
41
|
+
# Server : The provided signature does not match.
|
42
|
+
class SignatureDoesNotMatch < Error; end
|
43
|
+
|
44
|
+
# SES Errors
|
45
|
+
############################
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,313 @@
|
|
1
|
+
#:stopdoc:
|
2
|
+
|
3
|
+
class Hash
|
4
|
+
def to_query_string(include_question_mark = true)
|
5
|
+
query_string = ''
|
6
|
+
unless empty?
|
7
|
+
query_string << '?' if include_question_mark
|
8
|
+
query_string << inject([]) do |params, (key, value)|
|
9
|
+
params << "#{key}=#{value}"
|
10
|
+
end.join('&')
|
11
|
+
end
|
12
|
+
query_string
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_normalized_options
|
16
|
+
# Convert all option names to downcased strings, and replace underscores with hyphens
|
17
|
+
inject({}) do |normalized_options, (name, value)|
|
18
|
+
normalized_options[name.to_header] = value.to_s
|
19
|
+
normalized_options
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_normalized_options!
|
24
|
+
replace(to_normalized_options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class String
|
29
|
+
if RUBY_VERSION <= '1.9'
|
30
|
+
def previous!
|
31
|
+
self[-1] -= 1
|
32
|
+
self
|
33
|
+
end
|
34
|
+
else
|
35
|
+
def previous!
|
36
|
+
self[-1] = (self[-1].ord - 1).chr
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def previous
|
42
|
+
dup.previous!
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_header
|
46
|
+
downcase.tr('_', '-')
|
47
|
+
end
|
48
|
+
|
49
|
+
# ActiveSupport adds an underscore method to String so let's just use that one if
|
50
|
+
# we find that the method is already defined
|
51
|
+
def underscore
|
52
|
+
gsub(/::/, '/').
|
53
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
54
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
55
|
+
tr("-", "_").downcase
|
56
|
+
end unless public_method_defined? :underscore
|
57
|
+
|
58
|
+
if RUBY_VERSION >= '1.9'
|
59
|
+
def valid_utf8?
|
60
|
+
dup.force_encoding('UTF-8').valid_encoding?
|
61
|
+
end
|
62
|
+
else
|
63
|
+
def valid_utf8?
|
64
|
+
scan(Regexp.new('[^\x00-\xa0]', nil, 'u')) { |s| s.unpack('U') }
|
65
|
+
true
|
66
|
+
rescue ArgumentError
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# All paths in in S3 have to be valid unicode so this takes care of
|
72
|
+
# cleaning up any strings that aren't valid utf-8 according to String#valid_utf8?
|
73
|
+
if RUBY_VERSION >= '1.9'
|
74
|
+
def remove_extended!
|
75
|
+
sanitized_string = ''
|
76
|
+
each_byte do |byte|
|
77
|
+
character = byte.chr
|
78
|
+
sanitized_string << character if character.ascii_only?
|
79
|
+
end
|
80
|
+
sanitized_string
|
81
|
+
end
|
82
|
+
else
|
83
|
+
def remove_extended!
|
84
|
+
gsub!(/[\x80-\xFF]/) { "%02X" % $&[0] }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def remove_extended
|
89
|
+
dup.remove_extended!
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class CoercibleString < String
|
94
|
+
class << self
|
95
|
+
def coerce(string)
|
96
|
+
new(string).coerce
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def coerce
|
101
|
+
case self
|
102
|
+
when 'true'; true
|
103
|
+
when 'false'; false
|
104
|
+
# Don't coerce numbers that start with zero
|
105
|
+
when /^[1-9]+\d*$/; Integer(self)
|
106
|
+
when datetime_format; Time.parse(self)
|
107
|
+
else
|
108
|
+
self
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
# Lame hack since Date._parse is so accepting. S3 dates are of the form: '2006-10-29T23:14:47.000Z'
|
114
|
+
# so unless the string looks like that, don't even try, otherwise it might convert an object's
|
115
|
+
# key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001.
|
116
|
+
def datetime_format
|
117
|
+
/^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Symbol
|
122
|
+
def to_header
|
123
|
+
to_s.to_header
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
module Kernel
|
128
|
+
def __method__(depth = 0)
|
129
|
+
caller[depth][/`([^']+)'/, 1]
|
130
|
+
end if RUBY_VERSION <= '1.8.7'
|
131
|
+
|
132
|
+
def __called_from__
|
133
|
+
caller[1][/`([^']+)'/, 1]
|
134
|
+
end if RUBY_VERSION > '1.8.7'
|
135
|
+
|
136
|
+
def expirable_memoize(reload = false, storage = nil)
|
137
|
+
current_method = RUBY_VERSION > '1.8.7' ? __called_from__ : __method__(1)
|
138
|
+
storage = "@#{storage || current_method}"
|
139
|
+
if reload
|
140
|
+
instance_variable_set(storage, nil)
|
141
|
+
else
|
142
|
+
if cache = instance_variable_get(storage)
|
143
|
+
return cache
|
144
|
+
end
|
145
|
+
end
|
146
|
+
instance_variable_set(storage, yield)
|
147
|
+
end
|
148
|
+
|
149
|
+
def require_library_or_gem(library, gem_name = nil)
|
150
|
+
if RUBY_VERSION >= '1.9'
|
151
|
+
gem(gem_name || library, '>=0')
|
152
|
+
end
|
153
|
+
require library
|
154
|
+
rescue LoadError => library_not_installed
|
155
|
+
begin
|
156
|
+
require 'rubygems'
|
157
|
+
require library
|
158
|
+
rescue LoadError
|
159
|
+
raise library_not_installed
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class Object
|
165
|
+
def returning(value)
|
166
|
+
yield(value)
|
167
|
+
value
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class Module
|
172
|
+
def memoized(method_name)
|
173
|
+
original_method = "unmemoized_#{method_name}_#{Time.now.to_i}"
|
174
|
+
alias_method original_method, method_name
|
175
|
+
module_eval(<<-EVAL, __FILE__, __LINE__)
|
176
|
+
def #{method_name}(reload = false, *args, &block)
|
177
|
+
expirable_memoize(reload) do
|
178
|
+
send(:#{original_method}, *args, &block)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
EVAL
|
182
|
+
end
|
183
|
+
|
184
|
+
def constant(name, value)
|
185
|
+
unless const_defined?(name)
|
186
|
+
const_set(name, value)
|
187
|
+
module_eval(<<-EVAL, __FILE__, __LINE__)
|
188
|
+
def self.#{name.to_s.downcase}
|
189
|
+
#{name.to_s}
|
190
|
+
end
|
191
|
+
EVAL
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Transforms MarcelBucket into
|
196
|
+
#
|
197
|
+
# class MarcelBucket < AWS::S3::Bucket
|
198
|
+
# set_current_bucket_to 'marcel'
|
199
|
+
# end
|
200
|
+
def const_missing_from_s3_library(sym)
|
201
|
+
if sym.to_s =~ /^(\w+)(Bucket|S3Object)$/
|
202
|
+
const = const_set(sym, Class.new(AWS::S3.const_get($2)))
|
203
|
+
const.current_bucket = $1.underscore
|
204
|
+
const
|
205
|
+
else
|
206
|
+
const_missing_not_from_s3_library(sym)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
alias_method :const_missing_not_from_s3_library, :const_missing
|
210
|
+
alias_method :const_missing, :const_missing_from_s3_library
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
class Class # :nodoc:
|
215
|
+
def cattr_reader(*syms)
|
216
|
+
syms.flatten.each do |sym|
|
217
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
218
|
+
unless defined? @@#{sym}
|
219
|
+
@@#{sym} = nil
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.#{sym}
|
223
|
+
@@#{sym}
|
224
|
+
end
|
225
|
+
|
226
|
+
def #{sym}
|
227
|
+
@@#{sym}
|
228
|
+
end
|
229
|
+
EOS
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def cattr_writer(*syms)
|
234
|
+
syms.flatten.each do |sym|
|
235
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
236
|
+
unless defined? @@#{sym}
|
237
|
+
@@#{sym} = nil
|
238
|
+
end
|
239
|
+
|
240
|
+
def self.#{sym}=(obj)
|
241
|
+
@@#{sym} = obj
|
242
|
+
end
|
243
|
+
|
244
|
+
def #{sym}=(obj)
|
245
|
+
@@#{sym} = obj
|
246
|
+
end
|
247
|
+
EOS
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def cattr_accessor(*syms)
|
252
|
+
cattr_reader(*syms)
|
253
|
+
cattr_writer(*syms)
|
254
|
+
end
|
255
|
+
end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty?
|
256
|
+
|
257
|
+
module SelectiveAttributeProxy
|
258
|
+
def self.included(klass)
|
259
|
+
klass.extend(ClassMethods)
|
260
|
+
klass.class_eval(<<-EVAL, __FILE__, __LINE__)
|
261
|
+
cattr_accessor :attribute_proxy
|
262
|
+
cattr_accessor :attribute_proxy_options
|
263
|
+
|
264
|
+
# Default name for attribute storage
|
265
|
+
self.attribute_proxy = :attributes
|
266
|
+
self.attribute_proxy_options = {:exclusively => true}
|
267
|
+
|
268
|
+
private
|
269
|
+
# By default proxy all attributes
|
270
|
+
def proxiable_attribute?(name)
|
271
|
+
return true unless self.class.attribute_proxy_options[:exclusively]
|
272
|
+
send(self.class.attribute_proxy).has_key?(name)
|
273
|
+
end
|
274
|
+
|
275
|
+
def method_missing(method, *args, &block)
|
276
|
+
# Autovivify attribute storage
|
277
|
+
if method == self.class.attribute_proxy
|
278
|
+
ivar = "@\#{method}"
|
279
|
+
instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash)
|
280
|
+
instance_variable_get(ivar)
|
281
|
+
# Delegate to attribute storage
|
282
|
+
elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1)
|
283
|
+
attributes_hash_name = self.class.attribute_proxy
|
284
|
+
$2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first
|
285
|
+
else
|
286
|
+
super
|
287
|
+
end
|
288
|
+
end
|
289
|
+
EVAL
|
290
|
+
end
|
291
|
+
|
292
|
+
module ClassMethods
|
293
|
+
def proxy_to(attribute_name, options = {})
|
294
|
+
if attribute_name.is_a?(Hash)
|
295
|
+
options = attribute_name
|
296
|
+
else
|
297
|
+
self.attribute_proxy = attribute_name
|
298
|
+
end
|
299
|
+
self.attribute_proxy_options = options
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
|
305
|
+
class XmlGenerator < String #:nodoc:
|
306
|
+
attr_reader :xml
|
307
|
+
def initialize
|
308
|
+
@xml = Builder::XmlMarkup.new(:indent => 2, :target => self)
|
309
|
+
super()
|
310
|
+
build
|
311
|
+
end
|
312
|
+
end
|
313
|
+
#:startdoc:
|
@@ -0,0 +1,97 @@
|
|
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' && parsed.root == 'error'
|
46
|
+
end
|
47
|
+
|
48
|
+
def error
|
49
|
+
Error.new(parsed, self)
|
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
|
+
xml = XmlSimple.xml_in(body, parse_options)
|
58
|
+
xml["#{@action}Result"]
|
59
|
+
end
|
60
|
+
memoized :parsed
|
61
|
+
|
62
|
+
# It's expected that each subclass of Response will override this method with what part of response is relevant
|
63
|
+
def result
|
64
|
+
parsed
|
65
|
+
end
|
66
|
+
|
67
|
+
def inspect
|
68
|
+
"#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message]
|
69
|
+
end
|
70
|
+
end # class Response
|
71
|
+
|
72
|
+
# Requests whose response code is between 300 and 599 and contain an <Error></Error> in their body
|
73
|
+
# are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception
|
74
|
+
# that corresponds to the error in the response body. The exception object contains the ErrorResponse, so
|
75
|
+
# in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and
|
76
|
+
# its Error object which contains information about the ResponseError.
|
77
|
+
#
|
78
|
+
# begin
|
79
|
+
# Bucket.create(..)
|
80
|
+
# rescue ResponseError => exception
|
81
|
+
# exception.response
|
82
|
+
# # => <Error::Response>
|
83
|
+
# exception.response.error
|
84
|
+
# # => <Error>
|
85
|
+
# end
|
86
|
+
class Error < Response
|
87
|
+
def error?
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
def inspect
|
92
|
+
"#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end #module SES
|
96
|
+
end # module AWS
|
97
|
+
|