├── README.rst ├── setup.py └── ses ├── message.py └── __init__.py /README.rst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | #from ses import __version__ 5 | 6 | setup(name='ses', 7 | version='0.0.1', ##__version__, 8 | description="Python interface to AWS SES, django AWS SES backend", 9 | long_description="", 10 | keywords='', 11 | author='Xavier Grangier', 12 | author_email='grangier@gmail.com', 13 | url='', 14 | license='GPL', 15 | packages=find_packages(), 16 | include_package_data=True, 17 | zip_safe=False, 18 | install_requires=[ 19 | 'lxml', 20 | 'restkit', 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /ses/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | 3 | from email.Utils import formatdate 4 | from email import Charset 5 | from email.mime.text import MIMEText 6 | from email.mime.multipart import MIMEMultipart 7 | 8 | Charset.add_charset('utf-8', Charset.SHORTEST, None, 'utf-8') 9 | 10 | class SimpleEmailServiceMessageBase(object): 11 | """ 12 | 13 | """ 14 | def __init__(self, subject='', body='', from_email=None, 15 | to=None, bcc=None, headers=None, cc=None): 16 | """ 17 | 18 | """ 19 | # check subject 20 | assert subject, '"subject" cannot be empty' 21 | assert body, '"body" cannot be empty' 22 | 23 | if to: 24 | assert not isinstance(to, basestring), \ 25 | '"to" argument must be a list or tuple' 26 | self.to = list(to) 27 | else: 28 | self.to = [] 29 | if cc: 30 | assert not isinstance(cc, basestring), \ 31 | '"cc" argument must be a list or tuple' 32 | self.cc = list(cc) 33 | else: 34 | self.cc = [] 35 | if bcc: 36 | assert not isinstance(bcc, basestring), \ 37 | '"bcc" argument must be a list or tuple' 38 | self.bcc = list(bcc) 39 | else: 40 | self.bcc = [] 41 | 42 | self.from_email = from_email 43 | self.subject = subject 44 | self.body = body 45 | self.extra_headers = headers or {} 46 | 47 | 48 | def _create_msg_headers(self, msg): 49 | msg['Subject'] = self.subject 50 | msg['From'] = self.extra_headers.get('From', self.from_email) 51 | msg['To'] = ', '.join(self.to) 52 | if self.cc: 53 | msg['Cc'] = ', '.join(self.cc) 54 | 55 | # Email header names are case-insensitive (RFC 2045), so we have to 56 | # accommodate that when doing comparisons. 57 | header_names = [key.lower() for key in self.extra_headers] 58 | if 'date' not in header_names: 59 | msg['Date'] = formatdate() 60 | #if 'message-id' not in header_names: 61 | # msg['Message-ID'] = make_msgid() 62 | for name, value in self.extra_headers.items(): 63 | if name.lower() == 'from': # From is already handled 64 | continue 65 | msg[name] = value 66 | return msg 67 | 68 | def recipients(self): 69 | """ 70 | Returns a list of all recipients of the email (includes direct 71 | addressees as well as Cc and Bcc entries). 72 | """ 73 | return self.to + self.cc + self.bcc 74 | 75 | 76 | def send(self, fail_silently=False): 77 | ses = SimpleEmailService() 78 | ses.SendEmail(self) 79 | 80 | 81 | 82 | class SimpleEmailServiceMessage(SimpleEmailServiceMessageBase): 83 | """ 84 | 85 | """ 86 | def message(self): 87 | msg = MIMEText(self.body, 'plain', 'utf-8') 88 | return self._create_msg_headers(msg) 89 | 90 | 91 | 92 | 93 | 94 | class SimpleEmailServiceMessageAlternative(SimpleEmailServiceMessage): 95 | """ 96 | """ 97 | def __init__(self, subject='', body='', from_email=None, 98 | to=None, bcc=None, headers=None, alternatives=None, cc=None): 99 | """ 100 | """ 101 | super(SimpleEmailServiceMessageAlternative, self).\ 102 | __init__(subject, body, from_email, to, bcc, headers, cc) 103 | self.alternatives = alternatives or [] 104 | 105 | 106 | def attach_alternative(self, content, mimetype): 107 | """Attach an alternative content representation.""" 108 | assert content is not None 109 | assert mimetype is not "text/html" 110 | self.alternatives.append((content, mimetype)) 111 | 112 | 113 | 114 | def message(self): 115 | msg = MIMEMultipart('alternative') 116 | part = MIMEText(self.body, 'plain', 'utf-8') 117 | msg.attach(part) 118 | for alt in self.alternatives: 119 | part = MIMEText(alt[0], alt[1].split('/')[1], 'utf-8') 120 | msg.attach(part) 121 | return self._create_msg_headers(msg) 122 | 123 | 124 | -------------------------------------------------------------------------------- /ses/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | 3 | import restkit 4 | import time 5 | import hmac 6 | import hashlib 7 | import urllib 8 | import binascii 9 | from lxml import objectify 10 | 11 | 12 | version_info = (0, 0, 1) 13 | __version__ = ".".join(map(str, version_info)) 14 | 15 | 16 | 17 | class SimpleEmailServiceError(Exception): 18 | """ 19 | SimpleEmailService Exception 20 | """ 21 | 22 | def __init__(self, value): 23 | self.value = value 24 | 25 | def __str__(self): 26 | error = objectify.fromstring(self.value['body']) 27 | msg = "SimpleEmailService Error: Type:%s Code: %s Message:%s Request Id: %s" % \ 28 | (error.Error.Type, error.Error.Code, 29 | error.Error.Message, error.RequestId) 30 | return repr(msg) 31 | 32 | 33 | 34 | class SimpleEmailServiceRequest(object): 35 | """ 36 | SimpleEmailService Request 37 | """ 38 | def __init__(self, ses, method): 39 | self.ses = ses 40 | self.method = method 41 | self.parameters = {} 42 | self.date = time.strftime("%a, %d %b %Y %X GMT", time.gmtime()) 43 | self.headers = { 44 | 'user_agent':'SimpleEmailService/Python', 45 | 'Date': self.date, 46 | 'Host':self.ses.host 47 | } 48 | 49 | 50 | 51 | def getSignature(self): 52 | """Compute the signature""" 53 | hashed = hmac.new(self.ses.secretKey, self.date, hashlib.sha256) 54 | return binascii.b2a_base64(hashed.digest())[:-1] 55 | 56 | 57 | 58 | def setAuth(self): 59 | auth = 'AWS3-HTTPS AWSAccessKeyId=%s' % self.ses.accessKey; 60 | auth += ',Algorithm=HmacSHA256,Signature=%s' % self.getSignature(); 61 | self.headers.update({'X-Amzn-Authorization': auth}) 62 | 63 | 64 | def setParameter(self, key, value): 65 | self.parameters.update({key:value}) 66 | 67 | 68 | 69 | def getRequest(self): 70 | """ 71 | 72 | """ 73 | query = urllib.urlencode(self.parameters) 74 | url = 'https://' + self.ses.host + '/?' + query 75 | req = restkit.request(url, method=self.method, headers=self.headers) 76 | body = req.body_string() 77 | return { 78 | 'status': req.status_int, 79 | 'body': body 80 | } 81 | 82 | 83 | 84 | def postRequest(self): 85 | """ 86 | 87 | """ 88 | url = 'https://' + self.ses.host + '/' 89 | req = restkit.request(url, method=self.method, \ 90 | body=self.parameters, headers=self.headers) 91 | body = req.body_string() 92 | return { 93 | 'status': req.status_int, 94 | 'body': body 95 | } 96 | 97 | 98 | 99 | def response(self): 100 | self.setAuth() 101 | if self.method == 'POST': 102 | return self.postRequest() 103 | return self.getRequest() 104 | 105 | 106 | 107 | 108 | class SimpleEmailService(object): 109 | 110 | def __init__(self, accessKey=None, secretKey=None, 111 | host='email.us-east-1.amazonaws.com'): 112 | 113 | assert accessKey, '"accessKey" cannot be empty' 114 | assert secretKey, '"secretKey" cannot be empty' 115 | 116 | self.accessKey = accessKey 117 | self.secretKey = secretKey 118 | self.host = host 119 | 120 | 121 | 122 | def processResponse(self, response): 123 | status = response.get('status') 124 | print status 125 | body = response.get('body') 126 | if status == 200: 127 | return objectify.fromstring(body) 128 | raise SimpleEmailServiceError(response) 129 | 130 | 131 | 132 | def DeleteVerifiedEmailAddress(self, EmailAddress): 133 | """ 134 | Deletes the specified email address from the list 135 | of verified addresses. 136 | """ 137 | sesReq = SimpleEmailServiceRequest(self, 'DELETE') 138 | sesReq.setParameter('Action', 'DeleteVerifiedEmailAddress') 139 | sesReq.setParameter('EmailAddress', EmailAddress) 140 | response = self.processResponse(sesReq.response()) 141 | return { 142 | 'RequestId': response.ResponseMetadata.RequestId, 143 | } 144 | 145 | 146 | 147 | def GetSendQuota(self): 148 | """ 149 | Returns the user's current sending limits. 150 | """ 151 | sesReq = SimpleEmailServiceRequest(self, 'GET') 152 | sesReq.setParameter('Action', 'GetSendQuota') 153 | response = self.processResponse(sesReq.response()) 154 | return { 155 | 'Max24HourSend': response.GetSendQuotaResult.Max24HourSend, 156 | 'MaxSendRate': response.GetSendQuotaResult.MaxSendRate, 157 | 'SentLast24Hours': response.GetSendQuotaResult.SentLast24Hours, 158 | 'RequestId': response.ResponseMetadata.RequestId, 159 | } 160 | 161 | 162 | 163 | def GetSendStatistics(self): 164 | """ 165 | Returns the user's sending statistics. 166 | The result is a list of data points, 167 | representing the last two weeks of sending activity. 168 | """ 169 | sesReq = SimpleEmailServiceRequest(self, 'GET') 170 | sesReq.setParameter('Action', 'GetSendStatistics') 171 | response = self.processResponse(sesReq.response()) 172 | 173 | # member 174 | members = [{'DeliveryAttempts': i.DeliveryAttempts, 175 | 'Timestamp': i.Timestamp, 176 | 'Rejects': i.Rejects, 177 | 'Bounces': i.Bounces, 178 | 'Complaints': i.Complaints } for i in \ 179 | response.GetSendStatisticsResult.SendDataPoints.iterchildren()] 180 | 181 | return { 182 | 'GetSendStatisticsResult': members, 183 | 'RequestId': response.ResponseMetadata.RequestId, 184 | } 185 | 186 | 187 | def ListVerifiedEmailAddresses(self): 188 | """ 189 | Returns a list containing all of the email addresses 190 | that have been verified. 191 | """ 192 | sesReq = SimpleEmailServiceRequest(self, 'GET') 193 | sesReq.setParameter('Action', 'ListVerifiedEmailAddresses') 194 | response = self.processResponse(sesReq.response()) 195 | 196 | # emails list 197 | emails = [ i for i in response.ListVerifiedEmailAddressesResult.\ 198 | VerifiedEmailAddresses.iterchildren()] 199 | return { 200 | 'VerifiedEmailAddresses': emails, 201 | 'RequestId': response.ResponseMetadata.RequestId, 202 | } 203 | 204 | 205 | def SendEmail(self, Message): 206 | """ 207 | Composes an email message based on input data, and 208 | then immediately queues the message for sending. 209 | """ 210 | sesReq = SimpleEmailServiceRequest(self, 'POST') 211 | sesReq.setParameter('Action', 'SendEmail') 212 | 213 | # to 214 | for c, to in enumerate(Message.to): 215 | sesReq.setParameter('Destination.ToAddresses.member.%s' % \ 216 | (c + 1), to) 217 | # cc 218 | for c, cc in enumerate(Message.cc): 219 | sesReq.setParameter('Destination.CcAddresses.member.%s' % \ 220 | (c + 1), cc) 221 | # bcc 222 | for c, bcc in enumerate(Message.bcc): 223 | sesReq.setParameter('Destination.BccAddresses.member.%s' % \ 224 | (c + 1), bcc) 225 | # replyto 226 | 227 | # from 228 | sesReq.setParameter('Source', Message.from_email) 229 | sesReq.setParameter('ReturnPath', Message.from_email); 230 | # subject 231 | sesReq.setParameter('Message.Subject.Data', Message.subject) 232 | sesReq.setParameter('Message.Subject.Charset', 'utf-8') 233 | # text body 234 | sesReq.setParameter('Message.Body.Text.Data', Message.body) 235 | sesReq.setParameter('Message.Body.Text.Charset', 'utf-8') 236 | # html body 237 | if hasattr(Message, 'body_html'): 238 | sesReq.setParameter('Message.Body.Html.Data', Message.body_html) 239 | sesReq.setParameter('Message.Body.Html.Charset', 'utf-8') 240 | 241 | # send 242 | response = self.processResponse(sesReq.response()) 243 | return { 244 | 'MessageId': response.SendEmailResult.MessageId, 245 | 'RequestId': response.ResponseMetadata.RequestId, 246 | } 247 | 248 | 249 | 250 | def SendRawEmail(self, from_email, recipients, message_string): 251 | """ 252 | Sends an email message, with header and content specified by the client. 253 | The SendRawEmail action is useful for sending multipart MIME emails. 254 | The raw text of the message must comply with Internet email standards; 255 | otherwise, the message cannot be sent. 256 | """ 257 | base64_msg = binascii.b2a_base64(message_string)[:-1] 258 | sesReq = SimpleEmailServiceRequest(self, 'POST') 259 | sesReq.setParameter('Action', 'SendRawEmail') 260 | sesReq.setParameter('Source', from_email) 261 | sesReq.setParameter('RawMessage.Data', base64_msg) 262 | for c, i in enumerate(recipients): 263 | sesReq.setParameter('Destinations.member.%s' % (c+1), i) 264 | 265 | #r = sesReq.response() 266 | #print r 267 | response = self.processResponse(sesReq.response()) 268 | return { 269 | 'MessageId': response.SendRawEmailResult.MessageId, 270 | 'RequestId': response.ResponseMetadata.RequestId, 271 | } 272 | 273 | 274 | 275 | def VerifyEmailAddress(self, EmailAddress): 276 | """ 277 | Verifies an email address. 278 | This action causes a confirmation email message 279 | to be sent to the specified address. 280 | """ 281 | sesReq = SimpleEmailServiceRequest(self, 'GET') 282 | sesReq.setParameter('Action', 'VerifyEmailAddress') 283 | sesReq.setParameter('EmailAddress', EmailAddress) 284 | response = self.processResponse(sesReq.response()) 285 | return { 286 | 'RequestId': response.ResponseMetadata.RequestId, 287 | } 288 | 289 | 290 | 291 | 292 | --------------------------------------------------------------------------------