diff --git a/.gitignore b/.gitignore index 1caf74b..5a11797 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.swp *.swo pip-log.txt* +amazon_ses.egg-info/ *.swn # Emacs backup files & locks \#* diff --git a/README.txt b/README similarity index 90% rename from README.txt rename to README index d7241ab..97d8863 100644 --- a/README.txt +++ b/README @@ -2,6 +2,11 @@ Python API for Amazon Simple Email Service http://aws.amazon.com/ses/ =============== +Installation: +============= +python setup.py build +python setup.py install + Description =============== Here is an example of how to send an email using the API: @@ -18,18 +23,18 @@ message = EmailMessage() message.subject = 'Hello from Amazon SES! Test subject' message.bodyText = 'This is body text of test message.' -result = amazonSes.sendEmail('username@yourdomaintest.com', 'testmail@yourdomaintest.com', message) +result = amazonSes.sendEmail('username@yourdomaintest.com', 'testmail@yourdomaintest.com', message) print result.requestId print result.messageId -I want you to notice that in case Amazon returns some error, the API will raise an exception AmazonError which will contain errorType, code and message. +I want you to notice that in case Amazon returns some error, the API will raise an exception AmazonError which will contain errorType, code and message. If your Amazon SES account is not switched to production use, you can send a message only from/to verified email addresses. Here is an example of how to verify the email using the API: result = amazonSes.verifyEmailAddress('username@yourdomaintest.com') print result.requestId -You will receive the confirmation with a link which you should click to verify your email. +You will receive the confirmation with a link which you should click to verify your email. An example of how to receive information about SendQuota: @@ -44,7 +49,7 @@ VerifyEmailAddress DeleteVerifiedEmailAddress GetSendQuota ListVerifiedEmailAddresses -Methods return instances of AmazonResult or derived class (for example, method amazonSes.getSendQuota() returns the instance of AmazonSendQuota). +Methods return instances of AmazonResult or derived class (for example, method amazonSes.getSendQuota() returns the instance of AmazonSendQuota). Author =============== diff --git a/amazon_ses/__init__.py b/amazon_ses/__init__.py new file mode 100644 index 0000000..0982cab --- /dev/null +++ b/amazon_ses/__init__.py @@ -0,0 +1 @@ +from amazon_ses import * diff --git a/amazon_ses.py b/amazon_ses/amazon_ses.py similarity index 75% rename from amazon_ses.py rename to amazon_ses/amazon_ses.py index 046ed48..8047bf5 100644 --- a/amazon_ses.py +++ b/amazon_ses/amazon_ses.py @@ -1,4 +1,4 @@ -#Copyright (c) 2011 Vladimir Pankratiev http://tagmask.com +#Copyright (c) 2011 Vladimir Pankratiev http://tagmask.com # #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal @@ -24,8 +24,13 @@ import hmac import logging import base64 -from datetime import datetime -from xml.etree.ElementTree import XML +import datetime + +# Try to import the (much faster) C version of ElementTree +try: + from xml.etree import cElementTree as ET +except ImportError: + from xml.etree import ElementTree as ET log = logging.getLogger(__name__) @@ -34,14 +39,14 @@ def __init__(self, accessKeyID, secretAccessKey): self._accessKeyID = accessKeyID self._secretAccessKey = secretAccessKey self._responseParser = AmazonResponseParser() - + def _getSignature(self, dateValue): h = hmac.new(key=self._secretAccessKey, msg=dateValue, digestmod=hashlib.sha256) return base64.b64encode(h.digest()).decode() - + def _getHeaders(self): headers = { 'Content-type': 'application/x-www-form-urlencoded' } - d = datetime.utcnow() + d = datetime.datetime.utcnow() dateValue = d.strftime('%a, %d %b %Y %H:%M:%S GMT') headers['Date'] = dateValue signature = self._getSignature(dateValue) @@ -51,7 +56,7 @@ def _getHeaders(self): def _performAction(self, actionName, params=None): if not params: params = {} - params['Action'] = actionName + params['Action'] = actionName #https://email.us-east-1.amazonaws.com/ conn = httplib.HTTPSConnection('email.us-east-1.amazonaws.com') params = urllib.urlencode(params) @@ -60,36 +65,38 @@ def _performAction(self, actionName, params=None): responseResult = response.read() conn.close() return self._responseParser.parse(actionName, response.status, response.reason, responseResult) - + def verifyEmailAddress(self, emailAddress): params = { 'EmailAddress': emailAddress } return self._performAction('VerifyEmailAddress', params) - + def deleteVerifiedEmailAddress(self, emailAddress): params = { 'EmailAddress': emailAddress } return self._performAction('DeleteVerifiedEmailAddress', params) - + def getSendQuota(self): return self._performAction('GetSendQuota') - + def getSendStatistics(self): return self._performAction('GetSendStatistics') - + def listVerifiedEmailAddresses(self): return self._performAction('ListVerifiedEmailAddresses') - - def sendEmail(self, source, toAddresses, message, replyToAddresses=None, returnPath=None, ccAddresses=None, bccAddresses=None): - params = { 'Source': source } + + def sendEmail(self, source, toAddresses, message, replyToAddresses=[], returnPath=None, ccAddresses=None, bccAddresses=None): + params = {'Source': source} + for index, replyAddress in enumerate(replyToAddresses): + params['ReplyToAddresses.member.%d' % index+1] = replyAddress for objName, addresses in zip(["ToAddresses", "CcAddresses", "BccAddresses"], [toAddresses, ccAddresses, bccAddresses]): if addresses: if not isinstance(addresses, basestring) and getattr(addresses, '__iter__', False): for i, address in enumerate(addresses, 1): params['Destination.%s.member.%d' % (objName, i)] = address else: - params['Destination.%s.member.1' % objName] = addresses + params['Destination.%s.member.1' % objName] = addresses if not returnPath: returnPath = source - params['ReturnPath'] = returnPath + params['ReturnPath'] = returnPath params['Message.Subject.Charset'] = message.charset params['Message.Subject.Data'] = message.subject if message.bodyText: @@ -103,11 +110,11 @@ def sendEmail(self, source, toAddresses, message, replyToAddresses=None, returnP class EmailMessage: - def __init__(self): + def __init__(self, subject=None, bodyHtml=None, bodyText=None): self.charset = 'UTF-8' - self.subject = None - self.bodyHtml = None - self.bodyText = None + self.subject = subject + self.bodyHtml = bodyHtml + self.bodyText = bodyText @@ -116,56 +123,60 @@ def __init__(self, errorType, code, message): self.errorType = errorType self.code = code self.message = message - + def __str__(self): + return self.message + class AmazonAPIError(Exception): def __init__(self, message): self.message = message - - - + def __str__(self): + return self.message + + class AmazonResult: def __init__(self, requestId): self.requestId = requestId - + class AmazonSendEmailResult(AmazonResult): def __init__(self, requestId, messageId): self.requestId = requestId self.messageId = messageId - + class AmazonSendQuota(AmazonResult): def __init__(self, requestId, max24HourSend, maxSendRate, sentLast24Hours): self.requestId = requestId self.max24HourSend = max24HourSend self.maxSendRate = maxSendRate self.sentLast24Hours = sentLast24Hours - + class AmazonSendDataPoint: - def __init__(self, bounces, complaints, deliveryAttempts, rejects, timestamp): - self.bounces = bounces - self.complaints = complaints - self.deliveryAttempts = deliveryAttempts - self.rejects = rejects - self.timestamp = timestamp - + def __init__(self, xmlResponse, member): + self.timestamp = datetime.datetime.strptime(xmlResponse.getChildTextFromNode(member, 'Timestamp'), '%Y-%m-%dT%H:%M:%SZ') + self.deliveryAttempts = int(xmlResponse.getChildTextFromNode(member, 'DeliveryAttempts')) + self.bounces = int(xmlResponse.getChildTextFromNode(member, 'Bounces')) + self.complaints = int(xmlResponse.getChildTextFromNode(member, 'Complaints')) + self.rejects = int(xmlResponse.getChildTextFromNode(member, 'Rejects')) + class AmazonSendStatistics(AmazonResult): def __init__(self, requestId): self.requestId = requestId - self.members = [] - + self.members = [] + class AmazonVerifiedEmails(AmazonSendStatistics): pass - + class AmazonResponseParser: + # FIXME: This XML structure is way too rigid and complex, tastes like Java - needs more flexibility class XmlResponse: def __init__(self, str): - self._rootElement = XML(str) + self._rootElement = ET.XML(str) self._namespace = self._rootElement.tag[1:].split("}")[0] - + def checkResponseName(self, name): if self._rootElement.tag == self._fixTag(self._namespace, name): return True else: - raise AmazonAPIError('ErrorResponse is invalid.') + raise AmazonAPIError('ErrorResponse is invalid.') def checkActionName(self, actionName): if self._rootElement.tag == self._fixTag(self._namespace, ('%sResponse' % actionName)): @@ -180,52 +191,71 @@ def getChild(self, *itemPath): else: raise AmazonAPIError('Node with the specified path was not found.') - def getChildText(self, *itemPath): - node = self.getChild(*itemPath) + def getChildText(self, *itemPath): + node = self.getChild(*itemPath) return node.text + def getChildFromNode(self, node, *itemPath): + child = self._findNode(node, self._namespace, *itemPath) + if child is not None: + return child + else: + raise AmazonAPIError('Node with the specified path was not found.') + + def getChildTextFromNode(self, node, *itemPath): + child = self.getChildFromNode (node, *itemPath) + return child.text + + def getChildren(self, node): + return node.getchildren() + def _fixTag(self, namespace, tag): return '{%s}%s' % (namespace, tag) - + def _findNode(self, rootElement, namespace, *args): match = '.' for s in args: - match += '/{%s}%s' % (namespace, s) - return rootElement.find(match) - + match += '/{%s}%s' % (namespace, s) + return rootElement.find(match) + def __init__(self): self._simpleResultActions = ['DeleteVerifiedEmailAddress', 'VerifyEmailAddress'] - - def _parseSimpleResult(self, actionName, xmlResponse): + + def _parseSimpleResult(self, actionName, xmlResponse): if xmlResponse.checkActionName(actionName): requestId = xmlResponse.getChildText('ResponseMetadata', 'RequestId') return AmazonResult(requestId) - + def _parseSendQuota(self, actionName, xmlResponse): if xmlResponse.checkActionName(actionName): requestId = xmlResponse.getChildText('ResponseMetadata', 'RequestId') - value = xmlResponse.getChildText('GetSendQuotaResult', 'Max24HourSend') + value = xmlResponse.getChildText('GetSendQuotaResult', 'Max24HourSend') max24HourSend = float(value) value = xmlResponse.getChildText('GetSendQuotaResult', 'MaxSendRate') maxSendRate = float(value) value = xmlResponse.getChildText('GetSendQuotaResult', 'SentLast24Hours') sentLast24Hours = float(value) return AmazonSendQuota(requestId, max24HourSend, maxSendRate, sentLast24Hours) - - #def _parseSendStatistics(self, actionName, xmlResponse): - # if xmlResponse.checkActionName(actionName): - # requestId = xmlResponse.getChildText('ResponseMetadata', 'RequestId') + + def _parseSendStatistics(self, actionName, xmlResponse): + if xmlResponse.checkActionName(actionName): + requestId = xmlResponse.getChildText('ResponseMetadata', 'RequestId') + result = AmazonSendStatistics(requestId) + send_data_points = xmlResponse.getChild('GetSendStatisticsResult', 'SendDataPoints') + for member in send_data_points: + result.members.append(AmazonSendDataPoint(xmlResponse, member)) + return result def _parseListVerifiedEmails(self, actionName, xmlResponse): if xmlResponse.checkActionName(actionName): requestId = xmlResponse.getChildText('ResponseMetadata', 'RequestId') node = xmlResponse.getChild('ListVerifiedEmailAddressesResult', 'VerifiedEmailAddresses') result = AmazonVerifiedEmails(requestId) - for addr in node: + for addr in node: result.members.append(addr.text) return result - + def _parseSendEmail(self, actionName, xmlResponse): if xmlResponse.checkActionName(actionName): requestId = xmlResponse.getChildText('ResponseMetadata', 'RequestId') @@ -238,13 +268,13 @@ def _raiseError(self, xmlResponse): code = xmlResponse.getChildText('Error', 'Code') message = xmlResponse.getChildText('Error', 'Message') raise AmazonError(errorType, code, message) - - def parse(self, actionName, statusCode, reason, responseResult): + + def parse(self, actionName, statusCode, reason, responseResult): xmlResponse = self.XmlResponse(responseResult) log.info('Response status code: %s, reason: %s', statusCode, reason) log.debug(responseResult) - result = None + result = None if statusCode != 200: self._raiseError(xmlResponse) else: @@ -254,10 +284,10 @@ def parse(self, actionName, statusCode, reason, responseResult): result = self._parseSendEmail(actionName, xmlResponse) elif actionName == 'GetSendQuota': result = self._parseSendQuota(actionName, xmlResponse) - #elif actionName == 'GetSendStatistics': - # result = self._parseSendStatistics(actionName, xmlResponse) + elif actionName == 'GetSendStatistics': + result = self._parseSendStatistics(actionName, xmlResponse) elif actionName == 'ListVerifiedEmailAddresses': - result = self._parseListVerifiedEmails(actionName, xmlResponse) + result = self._parseListVerifiedEmails(actionName, xmlResponse) else: - raise AmazonAPIError('Action %s is not supported. Please contact: vladimir@tagmask.com' % (actionName,)) + raise AmazonAPIError('Action %s is not supported.' % (actionName,)) return result diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4eb8721 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +setup( + name = 'amazon-ses', + version = '0.2', + packages = find_packages(), + description = 'Python API for Amazon Simple Email Service', + author = 'Vladimir Pankratiev', + url = 'http://tagmask.com/vladimir/profile', + download_url = 'https://github.com/pankratiev/python-amazon-ses-api', + platforms = ['any'], +)