add ses core
This commit is contained in:
parent
41890225e6
commit
9a92e87f02
@ -4,4 +4,5 @@ logging.getLogger('boto').setLevel(logging.CRITICAL)
|
|||||||
from .dynamodb import mock_dynamodb
|
from .dynamodb import mock_dynamodb
|
||||||
from .ec2 import mock_ec2
|
from .ec2 import mock_ec2
|
||||||
from .s3 import mock_s3
|
from .s3 import mock_s3
|
||||||
|
from .ses import mock_ses
|
||||||
from .sqs import mock_sqs
|
from .sqs import mock_sqs
|
||||||
|
2
moto/ses/__init__.py
Normal file
2
moto/ses/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .models import ses_backend
|
||||||
|
mock_ses = ses_backend.decorator
|
72
moto/ses/models.py
Normal file
72
moto/ses/models.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import md5
|
||||||
|
|
||||||
|
from moto.core import BaseBackend
|
||||||
|
from .utils import get_random_message_id
|
||||||
|
|
||||||
|
|
||||||
|
class Message(object):
|
||||||
|
def __init__(self, message_id, source, subject, body, destination):
|
||||||
|
self.id = message_id
|
||||||
|
self.source = source
|
||||||
|
self.subject = subject
|
||||||
|
self.body = body
|
||||||
|
self.destination = destination
|
||||||
|
|
||||||
|
|
||||||
|
class RawMessage(object):
|
||||||
|
def __init__(self, message_id, source, destination, raw_data):
|
||||||
|
self.id = message_id
|
||||||
|
self.source = source
|
||||||
|
self.destination = destination
|
||||||
|
self.raw_data = raw_data
|
||||||
|
|
||||||
|
|
||||||
|
class SESQuota(object):
|
||||||
|
def __init__(self, messages):
|
||||||
|
self.messages = messages
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sent_past_24(self):
|
||||||
|
return len(self.messages)
|
||||||
|
|
||||||
|
|
||||||
|
class SESBackend(BaseBackend):
|
||||||
|
def __init__(self):
|
||||||
|
self.addresses = []
|
||||||
|
self.sent_messages = []
|
||||||
|
|
||||||
|
def verify_email_identity(self, address):
|
||||||
|
self.addresses.append(address)
|
||||||
|
|
||||||
|
def verify_domain(self, domain):
|
||||||
|
self.addresses.append(domain)
|
||||||
|
|
||||||
|
def list_identities(self):
|
||||||
|
return self.addresses
|
||||||
|
|
||||||
|
def delete_identity(self, identity):
|
||||||
|
self.addresses.remove(identity)
|
||||||
|
|
||||||
|
def send_email(self, source, subject, body, destination):
|
||||||
|
if source not in self.addresses:
|
||||||
|
return False
|
||||||
|
|
||||||
|
message_id = get_random_message_id()
|
||||||
|
message = Message(message_id, source, subject, body, destination)
|
||||||
|
self.sent_messages.append(message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def send_raw_email(self, source, destination, raw_data):
|
||||||
|
if source not in self.addresses:
|
||||||
|
return False
|
||||||
|
|
||||||
|
message_id = get_random_message_id()
|
||||||
|
message = RawMessage(message_id, source, destination, raw_data)
|
||||||
|
self.sent_messages.append(message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def get_send_quota(self):
|
||||||
|
return SESQuota(self.sent_messages)
|
||||||
|
|
||||||
|
|
||||||
|
ses_backend = SESBackend()
|
153
moto/ses/responses.py
Normal file
153
moto/ses/responses.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import re
|
||||||
|
from urlparse import parse_qs
|
||||||
|
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
from moto.core.utils import headers_to_dict, camelcase_to_underscores, method_names_from_class
|
||||||
|
from .models import ses_backend
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResponse(object):
|
||||||
|
def dispatch(self, uri, body, headers):
|
||||||
|
querystring = parse_qs(body)
|
||||||
|
|
||||||
|
self.path = uri.path
|
||||||
|
self.querystring = querystring
|
||||||
|
|
||||||
|
action = querystring['Action'][0]
|
||||||
|
action = camelcase_to_underscores(action)
|
||||||
|
|
||||||
|
method_names = method_names_from_class(self.__class__)
|
||||||
|
if action in method_names:
|
||||||
|
method = getattr(self, action)
|
||||||
|
return method()
|
||||||
|
raise NotImplementedError("The {} action has not been implemented".format(action))
|
||||||
|
|
||||||
|
|
||||||
|
class EmailResponse(BaseResponse):
|
||||||
|
|
||||||
|
def verify_email_identity(self):
|
||||||
|
address = self.querystring.get('EmailAddress')[0]
|
||||||
|
ses_backend.verify_email_identity(address)
|
||||||
|
template = Template(VERIFY_EMAIL_IDENTITY)
|
||||||
|
return template.render()
|
||||||
|
|
||||||
|
def list_identities(self):
|
||||||
|
identities = ses_backend.list_identities()
|
||||||
|
template = Template(LIST_IDENTITIES_RESPONSE)
|
||||||
|
return template.render(identities=identities)
|
||||||
|
|
||||||
|
def verify_domain_dkim(self):
|
||||||
|
domain = self.querystring.get('Domain')[0]
|
||||||
|
ses_backend.verify_domain(domain)
|
||||||
|
template = Template(VERIFY_DOMAIN_DKIM_RESPONSE)
|
||||||
|
return template.render()
|
||||||
|
|
||||||
|
def verify_domain_identity(self):
|
||||||
|
domain = self.querystring.get('Domain')[0]
|
||||||
|
ses_backend.verify_domain(domain)
|
||||||
|
template = Template(VERIFY_DOMAIN_DKIM_RESPONSE)
|
||||||
|
return template.render()
|
||||||
|
|
||||||
|
def delete_identity(self):
|
||||||
|
domain = self.querystring.get('Identity')[0]
|
||||||
|
ses_backend.delete_identity(domain)
|
||||||
|
template = Template(DELETE_IDENTITY_RESPONSE)
|
||||||
|
return template.render()
|
||||||
|
|
||||||
|
def send_email(self):
|
||||||
|
body = self.querystring.get('Message.Body.Text.Data')[0]
|
||||||
|
source = self.querystring.get('Source')[0]
|
||||||
|
subject = self.querystring.get('Message.Subject.Data')[0]
|
||||||
|
destination = self.querystring.get('Destination.ToAddresses.member.1')[0]
|
||||||
|
message = ses_backend.send_email(source, subject, body, destination)
|
||||||
|
if not message:
|
||||||
|
return "Did not have authority to send from email {}".format(source), dict(status=400)
|
||||||
|
template = Template(SEND_EMAIL_RESPONSE)
|
||||||
|
return template.render(message=message)
|
||||||
|
|
||||||
|
def send_raw_email(self):
|
||||||
|
source = self.querystring.get('Source')[0]
|
||||||
|
destination = self.querystring.get('Destinations.member.1')[0]
|
||||||
|
raw_data = self.querystring.get('RawMessage.Data')[0]
|
||||||
|
|
||||||
|
message = ses_backend.send_raw_email(source, destination, raw_data)
|
||||||
|
if not message:
|
||||||
|
return "Did not have authority to send from email {}".format(source), dict(status=400)
|
||||||
|
template = Template(SEND_RAW_EMAIL_RESPONSE)
|
||||||
|
return template.render(message=message)
|
||||||
|
|
||||||
|
def get_send_quota(self):
|
||||||
|
quota = ses_backend.get_send_quota()
|
||||||
|
template = Template(GET_SEND_QUOTA_RESPONSE)
|
||||||
|
return template.render(quota=quota)
|
||||||
|
|
||||||
|
|
||||||
|
VERIFY_EMAIL_IDENTITY = """<VerifyEmailIdentityResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||||
|
<VerifyEmailIdentityResult/>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>47e0ef1a-9bf2-11e1-9279-0100e8cf109a</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</VerifyEmailIdentityResponse>"""
|
||||||
|
|
||||||
|
LIST_IDENTITIES_RESPONSE = """<ListIdentitiesResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||||
|
<ListIdentitiesResult>
|
||||||
|
<Identities>
|
||||||
|
{% for identity in identities %}
|
||||||
|
<member>{{ identity }}</member>
|
||||||
|
{% endfor %}
|
||||||
|
</Identities>
|
||||||
|
</ListIdentitiesResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>cacecf23-9bf1-11e1-9279-0100e8cf109a</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</ListIdentitiesResponse>"""
|
||||||
|
|
||||||
|
VERIFY_DOMAIN_DKIM_RESPONSE = """<VerifyDomainDkimResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||||
|
<VerifyDomainDkimResult>
|
||||||
|
<DkimTokens>
|
||||||
|
<member>vvjuipp74whm76gqoni7qmwwn4w4qusjiainivf6sf</member>
|
||||||
|
<member>3frqe7jn4obpuxjpwpolz6ipb3k5nvt2nhjpik2oy</member>
|
||||||
|
<member>wrqplteh7oodxnad7hsl4mixg2uavzneazxv5sxi2</member>
|
||||||
|
</DkimTokens>
|
||||||
|
</VerifyDomainDkimResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>9662c15b-c469-11e1-99d1-797d6ecd6414</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</VerifyDomainDkimResponse>"""
|
||||||
|
|
||||||
|
DELETE_IDENTITY_RESPONSE = """<DeleteIdentityResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||||
|
<DeleteIdentityResult/>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>d96bd874-9bf2-11e1-8ee7-c98a0037a2b6</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</DeleteIdentityResponse>"""
|
||||||
|
|
||||||
|
SEND_EMAIL_RESPONSE = """<SendEmailResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||||
|
<SendEmailResult>
|
||||||
|
<MessageId>{{ message.id }}</MessageId>
|
||||||
|
</SendEmailResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>d5964849-c866-11e0-9beb-01a62d68c57f</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</SendEmailResponse>"""
|
||||||
|
|
||||||
|
SEND_RAW_EMAIL_RESPONSE = """<SendRawEmailResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||||
|
<SendRawEmailResult>
|
||||||
|
<MessageId>{{ message.id }}</MessageId>
|
||||||
|
</SendRawEmailResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>e0abcdfa-c866-11e0-b6d0-273d09173b49</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</SendRawEmailResponse>"""
|
||||||
|
|
||||||
|
GET_SEND_QUOTA_RESPONSE = """<GetSendQuotaResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
|
||||||
|
<GetSendQuotaResult>
|
||||||
|
<SentLast24Hours>{{ quota.sent_past_24 }}</SentLast24Hours>
|
||||||
|
<Max24HourSend>200.0</Max24HourSend>
|
||||||
|
<MaxSendRate>1.0</MaxSendRate>
|
||||||
|
</GetSendQuotaResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>273021c6-c866-11e0-b926-699e21c3af9e</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</GetSendQuotaResponse>"""
|
7
moto/ses/urls.py
Normal file
7
moto/ses/urls.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from .responses import EmailResponse
|
||||||
|
|
||||||
|
base_url = "https://email.us-east-1.amazonaws.com"
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'{0}/$'.format(base_url): EmailResponse().dispatch,
|
||||||
|
}
|
18
moto/ses/utils.py
Normal file
18
moto/ses/utils.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
|
||||||
|
def random_hex(length):
|
||||||
|
return ''.join(random.choice(string.lowercase) for x in range(length))
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_message_id():
|
||||||
|
return "{}-{}-{}-{}-{}-{}-{}".format(
|
||||||
|
random_hex(16),
|
||||||
|
random_hex(8),
|
||||||
|
random_hex(4),
|
||||||
|
random_hex(4),
|
||||||
|
random_hex(4),
|
||||||
|
random_hex(12),
|
||||||
|
random_hex(6),
|
||||||
|
)
|
86
tests/test_ses/test_ses.py
Normal file
86
tests/test_ses/test_ses.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import email
|
||||||
|
|
||||||
|
import boto
|
||||||
|
from boto.exception import BotoServerError
|
||||||
|
|
||||||
|
from sure import expect
|
||||||
|
|
||||||
|
from moto import mock_ses
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ses
|
||||||
|
def test_verify_email_identity():
|
||||||
|
conn = boto.connect_ses('the_key', 'the_secret')
|
||||||
|
conn.verify_email_identity("test@example.com")
|
||||||
|
|
||||||
|
identities = conn.list_identities()
|
||||||
|
address = identities['ListIdentitiesResponse']['ListIdentitiesResult']['Identities'][0]
|
||||||
|
address.should.equal('test@example.com')
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ses
|
||||||
|
def test_domain_verify():
|
||||||
|
conn = boto.connect_ses('the_key', 'the_secret')
|
||||||
|
|
||||||
|
conn.verify_domain_dkim("domain1.com")
|
||||||
|
conn.verify_domain_identity("domain2.com")
|
||||||
|
|
||||||
|
identities = conn.list_identities()
|
||||||
|
domains = list(identities['ListIdentitiesResponse']['ListIdentitiesResult']['Identities'])
|
||||||
|
domains.should.equal(['domain1.com', 'domain2.com'])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ses
|
||||||
|
def test_delete_identity():
|
||||||
|
conn = boto.connect_ses('the_key', 'the_secret')
|
||||||
|
conn.verify_email_identity("test@example.com")
|
||||||
|
|
||||||
|
conn.list_identities()['ListIdentitiesResponse']['ListIdentitiesResult']['Identities'].should.have.length_of(1)
|
||||||
|
conn.delete_identity("test@example.com")
|
||||||
|
conn.list_identities()['ListIdentitiesResponse']['ListIdentitiesResult']['Identities'].should.have.length_of(0)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ses
|
||||||
|
def test_send_email():
|
||||||
|
conn = boto.connect_ses('the_key', 'the_secret')
|
||||||
|
|
||||||
|
conn.send_email.when.called_with("test@example.com", "test subject",
|
||||||
|
"test body", "test_to@example.com").should.throw(BotoServerError)
|
||||||
|
|
||||||
|
conn.verify_email_identity("test@example.com")
|
||||||
|
conn.send_email("test@example.com", "test subject", "test body", "test_to@example.com")
|
||||||
|
|
||||||
|
send_quota = conn.get_send_quota()
|
||||||
|
sent_count = int(send_quota['GetSendQuotaResponse']['GetSendQuotaResult']['SentLast24Hours'])
|
||||||
|
sent_count.should.equal(1)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ses
|
||||||
|
def test_send_raw_email():
|
||||||
|
conn = boto.connect_ses('the_key', 'the_secret')
|
||||||
|
|
||||||
|
to = 'to@example.com'
|
||||||
|
message = email.mime.multipart.MIMEMultipart()
|
||||||
|
message['Subject'] = 'Test'
|
||||||
|
message['From'] = 'test@example.com'
|
||||||
|
message['To'] = to
|
||||||
|
|
||||||
|
# Message body
|
||||||
|
part = email.mime.text.MIMEText('test file attached')
|
||||||
|
message.attach(part)
|
||||||
|
|
||||||
|
# Attachment
|
||||||
|
part = email.mime.text.MIMEText('contents of test file here')
|
||||||
|
part.add_header('Content-Disposition', 'attachment; filename=test.txt')
|
||||||
|
message.attach(part)
|
||||||
|
|
||||||
|
conn.send_raw_email.when.called_with(source=message['From'], raw_message=message.as_string(),
|
||||||
|
destinations=message['To']).should.throw(BotoServerError)
|
||||||
|
|
||||||
|
conn.verify_email_identity("test@example.com")
|
||||||
|
conn.send_raw_email(source=message['From'], raw_message=message.as_string(),
|
||||||
|
destinations=message['To'])
|
||||||
|
|
||||||
|
send_quota = conn.get_send_quota()
|
||||||
|
sent_count = int(send_quota['GetSendQuotaResponse']['GetSendQuotaResult']['SentLast24Hours'])
|
||||||
|
sent_count.should.equal(1)
|
Loading…
Reference in New Issue
Block a user