diff --git a/amazon_ses.py b/amazon_ses.py index 046ed48..b0b87db 100644 --- a/amazon_ses.py +++ b/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,6 +24,7 @@ import hmac import logging import base64 +import re from datetime import datetime from xml.etree.ElementTree import XML @@ -38,7 +39,7 @@ def __init__(self, accessKeyID, secretAccessKey): 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() @@ -47,11 +48,11 @@ def _getHeaders(self): signature = self._getSignature(dateValue) headers['X-Amzn-Authorization'] = 'AWS3-HTTPS AWSAccessKeyId=%s, Algorithm=HMACSHA256, Signature=%s' % (self._accessKeyID, signature) return headers - + 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,24 +61,24 @@ 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 } for objName, addresses in zip(["ToAddresses", "CcAddresses", "BccAddresses"], [toAddresses, ccAddresses, bccAddresses]): @@ -86,10 +87,10 @@ def sendEmail(self, source, toAddresses, message, replyToAddresses=None, returnP 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: @@ -107,7 +108,7 @@ def __init__(self): self.charset = 'UTF-8' self.subject = None self.bodyHtml = None - self.bodyText = None + self.bodyText = None @@ -116,29 +117,27 @@ def __init__(self, errorType, code, message): self.errorType = errorType self.code = code self.message = message - + class AmazonAPIError(Exception): def __init__(self, message): self.message = 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 @@ -146,105 +145,129 @@ def __init__(self, bounces, complaints, deliveryAttempts, rejects, timestamp): self.deliveryAttempts = deliveryAttempts self.rejects = rejects self.timestamp = timestamp - + self.deliveries = int(deliveryAttempts) - int(bounces) - int(rejects) + class AmazonSendStatistics(AmazonResult): - def __init__(self, requestId): + def __init__(self, requestId, datapoints): self.requestId = requestId - self.members = [] - + self.datapoints = datapoints + class AmazonVerifiedEmails(AmazonSendStatistics): pass - + class AmazonResponseParser: class XmlResponse: def __init__(self, str): self._rootElement = 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)): return True else: raise AmazonAPIError('Response of action "%s" is invalid.' % actionName) - + def getChild(self, *itemPath): node = self._findNode(self._rootElement, self._namespace, *itemPath) if node != None: return node else: raise AmazonAPIError('Node with the specified path was not found.') - - def getChildText(self, *itemPath): - node = self.getChild(*itemPath) + + def getChildElementTag(self, child): #strip off namespace + return re.sub('^\{.*\}', "", child.tag) + + def getChildText(self, *itemPath): + node = self.getChild(*itemPath) return node.text - + 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') + dp = xmlResponse.getChild("GetSendStatisticsResult", "SendDataPoints") + dps = [] + for p in dp: + if xmlResponse.getChildElementTag(p) == "member": + for c in p: + tag = xmlResponse.getChildElementTag(c) + if tag == "DeliveryAttempts": + deliveryAttempts = c.text + elif tag == "Timestamp": + timestamp = datetime.strptime(c.text, "%Y-%m-%dT%H:%M:%SZ") + elif tag == "Rejects": + rejects = c.text + elif tag == "Bounces": + bounces = c.text + elif tag == "Complaints": + complaints = c.text + else: + raise AmazonAPIError("Unexpected data recieved in SendStatistics request") + dps.append(AmazonSendDataPoint(bounces, complaints, deliveryAttempts, rejects, timestamp)) + return AmazonSendStatistics(requestId, dps) + 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') messageId = xmlResponse.getChildText('SendEmailResult', 'MessageId') return AmazonSendEmailResult(requestId, messageId) - + def _raiseError(self, xmlResponse): if xmlResponse.checkResponseName('ErrorResponse'): errorType = xmlResponse.getChildText('Error', 'Type') 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 +277,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,)) return result