Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merging current branches + some additional changes #7

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*.swp
*.swo
pip-log.txt*
amazon_ses.egg-info/
*.swn
# Emacs backup files & locks
\#*
Expand Down
13 changes: 9 additions & 4 deletions README.txt → README
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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('[email protected]', '[email protected]', message)
result = amazonSes.sendEmail('[email protected]', '[email protected]', 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('[email protected]')
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:

Expand All @@ -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
===============
Expand Down
1 change: 1 addition & 0 deletions amazon_ses/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from amazon_ses import *
158 changes: 94 additions & 64 deletions amazon_ses.py → amazon_ses/amazon_ses.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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__)

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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



Expand All @@ -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)):
Expand All @@ -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')
Expand All @@ -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:
Expand All @@ -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: [email protected]' % (actionName,))
raise AmazonAPIError('Action %s is not supported.' % (actionName,))
return result
14 changes: 14 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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'],
)