comparison env/lib/python3.9/site-packages/boto/ses/connection.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # Copyright (c) 2010 Mitch Garnaat http://garnaat.org/
2 # Copyright (c) 2011 Harry Marr http://hmarr.com/
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining a
5 # copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
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
9 # persons to whom the Software is furnished to do so, subject to the fol-
10 # lowing conditions:
11 #
12 # The above copyright notice and this permission notice shall be included
13 # in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 # IN THE SOFTWARE.
22 import re
23 import base64
24
25 from boto.compat import six, urllib
26 from boto.connection import AWSAuthConnection
27 from boto.exception import BotoServerError
28 from boto.regioninfo import RegionInfo
29 import boto
30 import boto.jsonresponse
31 from boto.ses import exceptions as ses_exceptions
32
33
34 class SESConnection(AWSAuthConnection):
35
36 ResponseError = BotoServerError
37 DefaultRegionName = 'us-east-1'
38 DefaultRegionEndpoint = 'email.us-east-1.amazonaws.com'
39 APIVersion = '2010-12-01'
40
41 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
42 is_secure=True, port=None, proxy=None, proxy_port=None,
43 proxy_user=None, proxy_pass=None, debug=0,
44 https_connection_factory=None, region=None, path='/',
45 security_token=None, validate_certs=True, profile_name=None):
46 if not region:
47 region = RegionInfo(self, self.DefaultRegionName,
48 self.DefaultRegionEndpoint)
49 self.region = region
50 super(SESConnection, self).__init__(self.region.endpoint,
51 aws_access_key_id, aws_secret_access_key,
52 is_secure, port, proxy, proxy_port,
53 proxy_user, proxy_pass, debug,
54 https_connection_factory, path,
55 security_token=security_token,
56 validate_certs=validate_certs,
57 profile_name=profile_name)
58
59 def _required_auth_capability(self):
60 return ['ses']
61
62 def _build_list_params(self, params, items, label):
63 """Add an AWS API-compatible parameter list to a dictionary.
64
65 :type params: dict
66 :param params: The parameter dictionary
67
68 :type items: list
69 :param items: Items to be included in the list
70
71 :type label: string
72 :param label: The parameter list's name
73 """
74 if isinstance(items, six.string_types):
75 items = [items]
76 for i in range(1, len(items) + 1):
77 params['%s.%d' % (label, i)] = items[i - 1]
78
79 def _make_request(self, action, params=None):
80 """Make a call to the SES API.
81
82 :type action: string
83 :param action: The API method to use (e.g. SendRawEmail)
84
85 :type params: dict
86 :param params: Parameters that will be sent as POST data with the API
87 call.
88 """
89 ct = 'application/x-www-form-urlencoded; charset=UTF-8'
90 headers = {'Content-Type': ct}
91 params = params or {}
92 params['Action'] = action
93
94 for k, v in params.items():
95 if isinstance(v, six.text_type): # UTF-8 encode only if it's Unicode
96 params[k] = v.encode('utf-8')
97
98 response = super(SESConnection, self).make_request(
99 'POST',
100 '/',
101 headers=headers,
102 data=urllib.parse.urlencode(params)
103 )
104 body = response.read().decode('utf-8')
105 if response.status == 200:
106 list_markers = ('VerifiedEmailAddresses', 'Identities',
107 'DkimTokens', 'DkimAttributes',
108 'VerificationAttributes', 'SendDataPoints')
109 item_markers = ('member', 'item', 'entry')
110
111 e = boto.jsonresponse.Element(list_marker=list_markers,
112 item_marker=item_markers)
113 h = boto.jsonresponse.XmlHandler(e, None)
114 h.parse(body)
115 return e
116 else:
117 # HTTP codes other than 200 are considered errors. Go through
118 # some error handling to determine which exception gets raised,
119 self._handle_error(response, body)
120
121 def _handle_error(self, response, body):
122 """
123 Handle raising the correct exception, depending on the error. Many
124 errors share the same HTTP response code, meaning we have to get really
125 kludgey and do string searches to figure out what went wrong.
126 """
127 boto.log.error('%s %s' % (response.status, response.reason))
128 boto.log.error('%s' % body)
129
130 if "Address blacklisted." in body:
131 # Delivery failures happened frequently enough with the recipient's
132 # email address for Amazon to blacklist it. After a day or three,
133 # they'll be automatically removed, and delivery can be attempted
134 # again (if you write the code to do so in your application).
135 ExceptionToRaise = ses_exceptions.SESAddressBlacklistedError
136 exc_reason = "Address blacklisted."
137 elif "Email address is not verified." in body:
138 # This error happens when the "Reply-To" value passed to
139 # send_email() hasn't been verified yet.
140 ExceptionToRaise = ses_exceptions.SESAddressNotVerifiedError
141 exc_reason = "Email address is not verified."
142 elif "Daily message quota exceeded." in body:
143 # Encountered when your account exceeds the maximum total number
144 # of emails per 24 hours.
145 ExceptionToRaise = ses_exceptions.SESDailyQuotaExceededError
146 exc_reason = "Daily message quota exceeded."
147 elif "Maximum sending rate exceeded." in body:
148 # Your account has sent above its allowed requests a second rate.
149 ExceptionToRaise = ses_exceptions.SESMaxSendingRateExceededError
150 exc_reason = "Maximum sending rate exceeded."
151 elif "Domain ends with dot." in body:
152 # Recipient address ends with a dot/period. This is invalid.
153 ExceptionToRaise = ses_exceptions.SESDomainEndsWithDotError
154 exc_reason = "Domain ends with dot."
155 elif "Local address contains control or whitespace" in body:
156 # I think this pertains to the recipient address.
157 ExceptionToRaise = ses_exceptions.SESLocalAddressCharacterError
158 exc_reason = "Local address contains control or whitespace."
159 elif "Illegal address" in body:
160 # A clearly mal-formed address.
161 ExceptionToRaise = ses_exceptions.SESIllegalAddressError
162 exc_reason = "Illegal address"
163 # The re.search is to distinguish from the
164 # SESAddressNotVerifiedError error above.
165 elif re.search('Identity.*is not verified', body):
166 ExceptionToRaise = ses_exceptions.SESIdentityNotVerifiedError
167 exc_reason = "Identity is not verified."
168 elif "ownership not confirmed" in body:
169 ExceptionToRaise = ses_exceptions.SESDomainNotConfirmedError
170 exc_reason = "Domain ownership is not confirmed."
171 else:
172 # This is either a common AWS error, or one that we don't devote
173 # its own exception to.
174 ExceptionToRaise = self.ResponseError
175 exc_reason = response.reason
176
177 raise ExceptionToRaise(response.status, exc_reason, body)
178
179 def send_email(self, source, subject, body, to_addresses,
180 cc_addresses=None, bcc_addresses=None,
181 format='text', reply_addresses=None,
182 return_path=None, text_body=None, html_body=None):
183 """Composes an email message based on input data, and then immediately
184 queues the message for sending.
185
186 :type source: string
187 :param source: The sender's email address.
188
189 :type subject: string
190 :param subject: The subject of the message: A short summary of the
191 content, which will appear in the recipient's inbox.
192
193 :type body: string
194 :param body: The message body.
195
196 :type to_addresses: list of strings or string
197 :param to_addresses: The To: field(s) of the message.
198
199 :type cc_addresses: list of strings or string
200 :param cc_addresses: The CC: field(s) of the message.
201
202 :type bcc_addresses: list of strings or string
203 :param bcc_addresses: The BCC: field(s) of the message.
204
205 :type format: string
206 :param format: The format of the message's body, must be either "text"
207 or "html".
208
209 :type reply_addresses: list of strings or string
210 :param reply_addresses: The reply-to email address(es) for the
211 message. If the recipient replies to the
212 message, each reply-to address will
213 receive the reply.
214
215 :type return_path: string
216 :param return_path: The email address to which bounce notifications are
217 to be forwarded. If the message cannot be delivered
218 to the recipient, then an error message will be
219 returned from the recipient's ISP; this message
220 will then be forwarded to the email address
221 specified by the ReturnPath parameter.
222
223 :type text_body: string
224 :param text_body: The text body to send with this email.
225
226 :type html_body: string
227 :param html_body: The html body to send with this email.
228
229 """
230 format = format.lower().strip()
231 if body is not None:
232 if format == "text":
233 if text_body is not None:
234 raise Warning("You've passed in both a body and a "
235 "text_body; please choose one or the other.")
236 text_body = body
237 else:
238 if html_body is not None:
239 raise Warning("You've passed in both a body and an "
240 "html_body; please choose one or the other.")
241 html_body = body
242
243 params = {
244 'Source': source,
245 'Message.Subject.Data': subject,
246 }
247
248 if return_path:
249 params['ReturnPath'] = return_path
250
251 if html_body is not None:
252 params['Message.Body.Html.Data'] = html_body
253 if text_body is not None:
254 params['Message.Body.Text.Data'] = text_body
255
256 if(format not in ("text", "html")):
257 raise ValueError("'format' argument must be 'text' or 'html'")
258
259 if(not (html_body or text_body)):
260 raise ValueError("No text or html body found for mail")
261
262 self._build_list_params(params, to_addresses,
263 'Destination.ToAddresses.member')
264 if cc_addresses:
265 self._build_list_params(params, cc_addresses,
266 'Destination.CcAddresses.member')
267
268 if bcc_addresses:
269 self._build_list_params(params, bcc_addresses,
270 'Destination.BccAddresses.member')
271
272 if reply_addresses:
273 self._build_list_params(params, reply_addresses,
274 'ReplyToAddresses.member')
275
276 return self._make_request('SendEmail', params)
277
278 def send_raw_email(self, raw_message, source=None, destinations=None):
279 """Sends an email message, with header and content specified by the
280 client. The SendRawEmail action is useful for sending multipart MIME
281 emails, with attachments or inline content. The raw text of the message
282 must comply with Internet email standards; otherwise, the message
283 cannot be sent.
284
285 :type source: string
286 :param source: The sender's email address. Amazon's docs say:
287
288 If you specify the Source parameter, then bounce notifications and
289 complaints will be sent to this email address. This takes precedence
290 over any Return-Path header that you might include in the raw text of
291 the message.
292
293 :type raw_message: string
294 :param raw_message: The raw text of the message. The client is
295 responsible for ensuring the following:
296
297 - Message must contain a header and a body, separated by a blank line.
298 - All required header fields must be present.
299 - Each part of a multipart MIME message must be formatted properly.
300 - MIME content types must be among those supported by Amazon SES.
301 Refer to the Amazon SES Developer Guide for more details.
302 - Content must be base64-encoded, if MIME requires it.
303
304 :type destinations: list of strings or string
305 :param destinations: A list of destinations for the message.
306
307 """
308
309 if isinstance(raw_message, six.text_type):
310 raw_message = raw_message.encode('utf-8')
311
312 params = {
313 'RawMessage.Data': base64.b64encode(raw_message),
314 }
315
316 if source:
317 params['Source'] = source
318
319 if destinations:
320 self._build_list_params(params, destinations,
321 'Destinations.member')
322
323 return self._make_request('SendRawEmail', params)
324
325 def list_verified_email_addresses(self):
326 """Fetch a list of the email addresses that have been verified.
327
328 :rtype: dict
329 :returns: A ListVerifiedEmailAddressesResponse structure. Note that
330 keys must be unicode strings.
331 """
332 return self._make_request('ListVerifiedEmailAddresses')
333
334 def get_send_quota(self):
335 """Fetches the user's current activity limits.
336
337 :rtype: dict
338 :returns: A GetSendQuotaResponse structure. Note that keys must be
339 unicode strings.
340 """
341 return self._make_request('GetSendQuota')
342
343 def get_send_statistics(self):
344 """Fetches the user's sending statistics. The result is a list of data
345 points, representing the last two weeks of sending activity.
346
347 Each data point in the list contains statistics for a 15-minute
348 interval.
349
350 :rtype: dict
351 :returns: A GetSendStatisticsResponse structure. Note that keys must be
352 unicode strings.
353 """
354 return self._make_request('GetSendStatistics')
355
356 def delete_verified_email_address(self, email_address):
357 """Deletes the specified email address from the list of verified
358 addresses.
359
360 :type email_adddress: string
361 :param email_address: The email address to be removed from the list of
362 verified addreses.
363
364 :rtype: dict
365 :returns: A DeleteVerifiedEmailAddressResponse structure. Note that
366 keys must be unicode strings.
367 """
368 return self._make_request('DeleteVerifiedEmailAddress', {
369 'EmailAddress': email_address,
370 })
371
372 def verify_email_address(self, email_address):
373 """Verifies an email address. This action causes a confirmation email
374 message to be sent to the specified address.
375
376 :type email_adddress: string
377 :param email_address: The email address to be verified.
378
379 :rtype: dict
380 :returns: A VerifyEmailAddressResponse structure. Note that keys must
381 be unicode strings.
382 """
383 return self._make_request('VerifyEmailAddress', {
384 'EmailAddress': email_address,
385 })
386
387 def verify_domain_dkim(self, domain):
388 """
389 Returns a set of DNS records, or tokens, that must be published in the
390 domain name's DNS to complete the DKIM verification process. These
391 tokens are DNS ``CNAME`` records that point to DKIM public keys hosted
392 by Amazon SES. To complete the DKIM verification process, these tokens
393 must be published in the domain's DNS. The tokens must remain
394 published in order for Easy DKIM signing to function correctly.
395
396 After the tokens are added to the domain's DNS, Amazon SES will be able
397 to DKIM-sign email originating from that domain. To enable or disable
398 Easy DKIM signing for a domain, use the ``SetIdentityDkimEnabled``
399 action. For more information about Easy DKIM, go to the `Amazon SES
400 Developer Guide
401 <http://docs.amazonwebservices.com/ses/latest/DeveloperGuide>`_.
402
403 :type domain: string
404 :param domain: The domain name.
405
406 """
407 return self._make_request('VerifyDomainDkim', {
408 'Domain': domain,
409 })
410
411 def set_identity_dkim_enabled(self, identity, dkim_enabled):
412 """Enables or disables DKIM signing of email sent from an identity.
413
414 * If Easy DKIM signing is enabled for a domain name identity (e.g.,
415 * ``example.com``),
416 then Amazon SES will DKIM-sign all email sent by addresses under that
417 domain name (e.g., ``user@example.com``)
418 * If Easy DKIM signing is enabled for an email address, then Amazon SES
419 will DKIM-sign all email sent by that email address.
420
421 For email addresses (e.g., ``user@example.com``), you can only enable
422 Easy DKIM signing if the corresponding domain (e.g., ``example.com``)
423 has been set up for Easy DKIM using the AWS Console or the
424 ``VerifyDomainDkim`` action.
425
426 :type identity: string
427 :param identity: An email address or domain name.
428
429 :type dkim_enabled: bool
430 :param dkim_enabled: Specifies whether or not to enable DKIM signing.
431
432 """
433 return self._make_request('SetIdentityDkimEnabled', {
434 'Identity': identity,
435 'DkimEnabled': 'true' if dkim_enabled else 'false'
436 })
437
438 def get_identity_dkim_attributes(self, identities):
439 """Get attributes associated with a list of verified identities.
440
441 Given a list of verified identities (email addresses and/or domains),
442 returns a structure describing identity notification attributes.
443
444 :type identities: list
445 :param identities: A list of verified identities (email addresses
446 and/or domains).
447
448 """
449 params = {}
450 self._build_list_params(params, identities, 'Identities.member')
451 return self._make_request('GetIdentityDkimAttributes', params)
452
453 def list_identities(self):
454 """Returns a list containing all of the identities (email addresses
455 and domains) for a specific AWS Account, regardless of
456 verification status.
457
458 :rtype: dict
459 :returns: A ListIdentitiesResponse structure. Note that
460 keys must be unicode strings.
461 """
462 return self._make_request('ListIdentities')
463
464 def get_identity_verification_attributes(self, identities):
465 """Given a list of identities (email addresses and/or domains),
466 returns the verification status and (for domain identities)
467 the verification token for each identity.
468
469 :type identities: list of strings or string
470 :param identities: List of identities.
471
472 :rtype: dict
473 :returns: A GetIdentityVerificationAttributesResponse structure.
474 Note that keys must be unicode strings.
475 """
476 params = {}
477 self._build_list_params(params, identities,
478 'Identities.member')
479 return self._make_request('GetIdentityVerificationAttributes', params)
480
481 def verify_domain_identity(self, domain):
482 """Verifies a domain.
483
484 :type domain: string
485 :param domain: The domain to be verified.
486
487 :rtype: dict
488 :returns: A VerifyDomainIdentityResponse structure. Note that
489 keys must be unicode strings.
490 """
491 return self._make_request('VerifyDomainIdentity', {
492 'Domain': domain,
493 })
494
495 def verify_email_identity(self, email_address):
496 """Verifies an email address. This action causes a confirmation
497 email message to be sent to the specified address.
498
499 :type email_adddress: string
500 :param email_address: The email address to be verified.
501
502 :rtype: dict
503 :returns: A VerifyEmailIdentityResponse structure. Note that keys must
504 be unicode strings.
505 """
506 return self._make_request('VerifyEmailIdentity', {
507 'EmailAddress': email_address,
508 })
509
510 def delete_identity(self, identity):
511 """Deletes the specified identity (email address or domain) from
512 the list of verified identities.
513
514 :type identity: string
515 :param identity: The identity to be deleted.
516
517 :rtype: dict
518 :returns: A DeleteIdentityResponse structure. Note that keys must
519 be unicode strings.
520 """
521 return self._make_request('DeleteIdentity', {
522 'Identity': identity,
523 })
524
525 def set_identity_notification_topic(self, identity, notification_type, sns_topic=None):
526 """Sets an SNS topic to publish bounce or complaint notifications for
527 emails sent with the given identity as the Source. Publishing to topics
528 may only be disabled when feedback forwarding is enabled.
529
530 :type identity: string
531 :param identity: An email address or domain name.
532
533 :type notification_type: string
534 :param notification_type: The type of feedback notifications that will
535 be published to the specified topic.
536 Valid Values: Bounce | Complaint | Delivery
537
538 :type sns_topic: string or None
539 :param sns_topic: The Amazon Resource Name (ARN) of the Amazon Simple
540 Notification Service (Amazon SNS) topic.
541 """
542 params = {
543 'Identity': identity,
544 'NotificationType': notification_type
545 }
546 if sns_topic:
547 params['SnsTopic'] = sns_topic
548 return self._make_request('SetIdentityNotificationTopic', params)
549
550 def set_identity_feedback_forwarding_enabled(self, identity, forwarding_enabled=True):
551 """
552 Enables or disables SES feedback notification via email.
553 Feedback forwarding may only be disabled when both complaint and
554 bounce topics are set.
555
556 :type identity: string
557 :param identity: An email address or domain name.
558
559 :type forwarding_enabled: bool
560 :param forwarding_enabled: Specifies whether or not to enable feedback forwarding.
561 """
562 return self._make_request('SetIdentityFeedbackForwardingEnabled', {
563 'Identity': identity,
564 'ForwardingEnabled': 'true' if forwarding_enabled else 'false'
565 })