| OLD | NEW |
| 1 # Copyright (c) 2010 Mitch Garnaat http://garnaat.org/ | 1 # Copyright (c) 2010 Mitch Garnaat http://garnaat.org/ |
| 2 # Copyright (c) 2011 Harry Marr http://hmarr.com/ | 2 # Copyright (c) 2011 Harry Marr http://hmarr.com/ |
| 3 # | 3 # |
| 4 # Permission is hereby granted, free of charge, to any person obtaining a | 4 # Permission is hereby granted, free of charge, to any person obtaining a |
| 5 # copy of this software and associated documentation files (the | 5 # copy of this software and associated documentation files (the |
| 6 # "Software"), to deal in the Software without restriction, including | 6 # "Software"), to deal in the Software without restriction, including |
| 7 # without limitation the rights to use, copy, modify, merge, publish, dis- | 7 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 8 # tribute, sublicense, and/or sell copies of the Software, and to permit | 8 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 9 # persons to whom the Software is furnished to do so, subject to the fol- | 9 # persons to whom the Software is furnished to do so, subject to the fol- |
| 10 # lowing conditions: | 10 # lowing conditions: |
| (...skipping 10 matching lines...) Expand all Loading... |
| 21 # IN THE SOFTWARE. | 21 # IN THE SOFTWARE. |
| 22 | 22 |
| 23 from boto.connection import AWSAuthConnection | 23 from boto.connection import AWSAuthConnection |
| 24 from boto.exception import BotoServerError | 24 from boto.exception import BotoServerError |
| 25 from boto.regioninfo import RegionInfo | 25 from boto.regioninfo import RegionInfo |
| 26 import boto | 26 import boto |
| 27 import boto.jsonresponse | 27 import boto.jsonresponse |
| 28 | 28 |
| 29 import urllib | 29 import urllib |
| 30 import base64 | 30 import base64 |
| 31 from boto.ses import exceptions as ses_exceptions |
| 31 | 32 |
| 32 | 33 |
| 33 class SESConnection(AWSAuthConnection): | 34 class SESConnection(AWSAuthConnection): |
| 34 | 35 |
| 35 ResponseError = BotoServerError | 36 ResponseError = BotoServerError |
| 36 DefaultRegionName = 'us-east-1' | 37 DefaultRegionName = 'us-east-1' |
| 37 DefaultRegionEndpoint = 'email.us-east-1.amazonaws.com' | 38 DefaultRegionEndpoint = 'email.us-east-1.amazonaws.com' |
| 38 APIVersion = '2010-12-01' | 39 APIVersion = '2010-12-01' |
| 39 | 40 |
| 40 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, | 41 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, |
| (...skipping 23 matching lines...) Expand all Loading... |
| 64 :param items: Items to be included in the list | 65 :param items: Items to be included in the list |
| 65 | 66 |
| 66 :type label: string | 67 :type label: string |
| 67 :param label: The parameter list's name | 68 :param label: The parameter list's name |
| 68 """ | 69 """ |
| 69 if isinstance(items, basestring): | 70 if isinstance(items, basestring): |
| 70 items = [items] | 71 items = [items] |
| 71 for i in range(1, len(items) + 1): | 72 for i in range(1, len(items) + 1): |
| 72 params['%s.%d' % (label, i)] = items[i - 1] | 73 params['%s.%d' % (label, i)] = items[i - 1] |
| 73 | 74 |
| 74 | |
| 75 def _make_request(self, action, params=None): | 75 def _make_request(self, action, params=None): |
| 76 """Make a call to the SES API. | 76 """Make a call to the SES API. |
| 77 | 77 |
| 78 :type action: string | 78 :type action: string |
| 79 :param action: The API method to use (e.g. SendRawEmail) | 79 :param action: The API method to use (e.g. SendRawEmail) |
| 80 | 80 |
| 81 :type params: dict | 81 :type params: dict |
| 82 :param params: Parameters that will be sent as POST data with the API | 82 :param params: Parameters that will be sent as POST data with the API |
| 83 call. | 83 call. |
| 84 """ | 84 """ |
| 85 ct = 'application/x-www-form-urlencoded; charset=UTF-8' | 85 ct = 'application/x-www-form-urlencoded; charset=UTF-8' |
| 86 headers = {'Content-Type': ct} | 86 headers = {'Content-Type': ct} |
| 87 params = params or {} | 87 params = params or {} |
| 88 params['Action'] = action | 88 params['Action'] = action |
| 89 | 89 |
| 90 for k, v in params.items(): | 90 for k, v in params.items(): |
| 91 if isinstance(v, unicode): # UTF-8 encode only if it's Unicode | 91 if isinstance(v, unicode): # UTF-8 encode only if it's Unicode |
| 92 params[k] = v.encode('utf-8') | 92 params[k] = v.encode('utf-8') |
| 93 | 93 |
| 94 response = super(SESConnection, self).make_request( | 94 response = super(SESConnection, self).make_request( |
| 95 'POST', | 95 'POST', |
| 96 '/', | 96 '/', |
| 97 headers=headers, | 97 headers=headers, |
| 98 data=urllib.urlencode(params) | 98 data=urllib.urlencode(params) |
| 99 ) | 99 ) |
| 100 body = response.read() | 100 body = response.read() |
| 101 if response.status == 200: | 101 if response.status == 200: |
| 102 list_markers = ('VerifiedEmailAddresses', 'SendDataPoints') | 102 list_markers = ('VerifiedEmailAddresses', 'SendDataPoints') |
| 103 e = boto.jsonresponse.Element(list_marker=list_markers) | 103 e = boto.jsonresponse.Element(list_marker=list_markers) |
| 104 h = boto.jsonresponse.XmlHandler(e, None) | 104 h = boto.jsonresponse.XmlHandler(e, None) |
| 105 h.parse(body) | 105 h.parse(body) |
| 106 return e | 106 return e |
| 107 else: | 107 else: |
| 108 boto.log.error('%s %s' % (response.status, response.reason)) | 108 # HTTP codes other than 200 are considered errors. Go through |
| 109 boto.log.error('%s' % body) | 109 # some error handling to determine which exception gets raised, |
| 110 raise self.ResponseError(response.status, response.reason, body) | 110 self._handle_error(response, body) |
| 111 | 111 |
| 112 def _handle_error(self, response, body): |
| 113 """ |
| 114 Handle raising the correct exception, depending on the error. Many |
| 115 errors share the same HTTP response code, meaning we have to get really |
| 116 kludgey and do string searches to figure out what went wrong. |
| 117 """ |
| 118 boto.log.error('%s %s' % (response.status, response.reason)) |
| 119 boto.log.error('%s' % body) |
| 120 |
| 121 if "Address blacklisted." in body: |
| 122 # Delivery failures happened frequently enough with the recipient's |
| 123 # email address for Amazon to blacklist it. After a day or three, |
| 124 # they'll be automatically removed, and delivery can be attempted |
| 125 # again (if you write the code to do so in your application). |
| 126 ExceptionToRaise = ses_exceptions.SESAddressBlacklistedError |
| 127 exc_reason = "Address blacklisted." |
| 128 elif "Email address is not verified." in body: |
| 129 # This error happens when the "Reply-To" value passed to |
| 130 # send_email() hasn't been verified yet. |
| 131 ExceptionToRaise = ses_exceptions.SESAddressNotVerifiedError |
| 132 exc_reason = "Email address is not verified." |
| 133 elif "Daily message quota exceeded." in body: |
| 134 # Encountered when your account exceeds the maximum total number |
| 135 # of emails per 24 hours. |
| 136 ExceptionToRaise = ses_exceptions.SESDailyQuotaExceededError |
| 137 exc_reason = "Daily message quota exceeded." |
| 138 elif "Maximum sending rate exceeded." in body: |
| 139 # Your account has sent above its allowed requests a second rate. |
| 140 ExceptionToRaise = ses_exceptions.SESMaxSendingRateExceededError |
| 141 exc_reason = "Maximum sending rate exceeded." |
| 142 else: |
| 143 # This is either a common AWS error, or one that we don't devote |
| 144 # its own exception to. |
| 145 ExceptionToRaise = self.ResponseError |
| 146 exc_reason = response.reason |
| 147 |
| 148 raise ExceptionToRaise(response.status, exc_reason, body) |
| 112 | 149 |
| 113 def send_email(self, source, subject, body, to_addresses, cc_addresses=None, | 150 def send_email(self, source, subject, body, to_addresses, cc_addresses=None, |
| 114 bcc_addresses=None, format='text', reply_addresses=None, | 151 bcc_addresses=None, format='text', reply_addresses=None, |
| 115 return_path=None, text_body=None, html_body=None): | 152 return_path=None, text_body=None, html_body=None): |
| 116 """Composes an email message based on input data, and then immediately | 153 """Composes an email message based on input data, and then immediately |
| 117 queues the message for sending. | 154 queues the message for sending. |
| 118 | 155 |
| 119 :type source: string | 156 :type source: string |
| 120 :param source: The sender's email address. | 157 :param source: The sender's email address. |
| 121 | 158 |
| (...skipping 110 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 232 Refer to the Amazon SES Developer Guide for more details. | 269 Refer to the Amazon SES Developer Guide for more details. |
| 233 - Content must be base64-encoded, if MIME requires it. | 270 - Content must be base64-encoded, if MIME requires it. |
| 234 | 271 |
| 235 :type destinations: list of strings or string | 272 :type destinations: list of strings or string |
| 236 :param destinations: A list of destinations for the message. | 273 :param destinations: A list of destinations for the message. |
| 237 | 274 |
| 238 """ | 275 """ |
| 239 params = { | 276 params = { |
| 240 'RawMessage.Data': base64.b64encode(raw_message), | 277 'RawMessage.Data': base64.b64encode(raw_message), |
| 241 } | 278 } |
| 242 | 279 |
| 243 if source: | 280 if source: |
| 244 params['Source'] = source | 281 params['Source'] = source |
| 245 | 282 |
| 246 if destinations: | 283 if destinations: |
| 247 self._build_list_params(params, destinations, | 284 self._build_list_params(params, destinations, |
| 248 'Destinations.member') | 285 'Destinations.member') |
| 249 | 286 |
| 250 return self._make_request('SendRawEmail', params) | 287 return self._make_request('SendRawEmail', params) |
| 251 | 288 |
| 252 def list_verified_email_addresses(self): | 289 def list_verified_email_addresses(self): |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 303 :type email_adddress: string | 340 :type email_adddress: string |
| 304 :param email_address: The email address to be verified. | 341 :param email_address: The email address to be verified. |
| 305 | 342 |
| 306 :rtype: dict | 343 :rtype: dict |
| 307 :returns: A VerifyEmailAddressResponse structure. Note that keys must | 344 :returns: A VerifyEmailAddressResponse structure. Note that keys must |
| 308 be unicode strings. | 345 be unicode strings. |
| 309 """ | 346 """ |
| 310 return self._make_request('VerifyEmailAddress', { | 347 return self._make_request('VerifyEmailAddress', { |
| 311 'EmailAddress': email_address, | 348 'EmailAddress': email_address, |
| 312 }) | 349 }) |
| OLD | NEW |