diff --git a/moto/__init__.py b/moto/__init__.py
index e9a5815ed..0548f9653 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -4,4 +4,5 @@ logging.getLogger('boto').setLevel(logging.CRITICAL)
from .dynamodb import mock_dynamodb
from .ec2 import mock_ec2
from .s3 import mock_s3
+from .ses import mock_ses
from .sqs import mock_sqs
diff --git a/moto/ses/__init__.py b/moto/ses/__init__.py
new file mode 100644
index 000000000..77bc397fb
--- /dev/null
+++ b/moto/ses/__init__.py
@@ -0,0 +1,2 @@
+from .models import ses_backend
+mock_ses = ses_backend.decorator
diff --git a/moto/ses/models.py b/moto/ses/models.py
new file mode 100644
index 000000000..546ff5b77
--- /dev/null
+++ b/moto/ses/models.py
@@ -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()
\ No newline at end of file
diff --git a/moto/ses/responses.py b/moto/ses/responses.py
new file mode 100644
index 000000000..8ea3a7e7f
--- /dev/null
+++ b/moto/ses/responses.py
@@ -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 = """
+
+
+ 47e0ef1a-9bf2-11e1-9279-0100e8cf109a
+
+"""
+
+LIST_IDENTITIES_RESPONSE = """
+
+
+ {% for identity in identities %}
+ {{ identity }}
+ {% endfor %}
+
+
+
+ cacecf23-9bf1-11e1-9279-0100e8cf109a
+
+"""
+
+VERIFY_DOMAIN_DKIM_RESPONSE = """
+
+
+ vvjuipp74whm76gqoni7qmwwn4w4qusjiainivf6sf
+ 3frqe7jn4obpuxjpwpolz6ipb3k5nvt2nhjpik2oy
+ wrqplteh7oodxnad7hsl4mixg2uavzneazxv5sxi2
+
+
+
+ 9662c15b-c469-11e1-99d1-797d6ecd6414
+
+"""
+
+DELETE_IDENTITY_RESPONSE = """
+
+
+ d96bd874-9bf2-11e1-8ee7-c98a0037a2b6
+
+"""
+
+SEND_EMAIL_RESPONSE = """
+
+ {{ message.id }}
+
+
+ d5964849-c866-11e0-9beb-01a62d68c57f
+
+"""
+
+SEND_RAW_EMAIL_RESPONSE = """
+
+ {{ message.id }}
+
+
+ e0abcdfa-c866-11e0-b6d0-273d09173b49
+
+"""
+
+GET_SEND_QUOTA_RESPONSE = """
+
+ {{ quota.sent_past_24 }}
+ 200.0
+ 1.0
+
+
+ 273021c6-c866-11e0-b926-699e21c3af9e
+
+"""
diff --git a/moto/ses/urls.py b/moto/ses/urls.py
new file mode 100644
index 000000000..33d6a74e6
--- /dev/null
+++ b/moto/ses/urls.py
@@ -0,0 +1,7 @@
+from .responses import EmailResponse
+
+base_url = "https://email.us-east-1.amazonaws.com"
+
+urls = {
+ '{0}/$'.format(base_url): EmailResponse().dispatch,
+}
diff --git a/moto/ses/utils.py b/moto/ses/utils.py
new file mode 100644
index 000000000..b7edf9c4d
--- /dev/null
+++ b/moto/ses/utils.py
@@ -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),
+ )
diff --git a/tests/test_ses/test_ses.py b/tests/test_ses/test_ses.py
new file mode 100644
index 000000000..934742c6f
--- /dev/null
+++ b/tests/test_ses/test_ses.py
@@ -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)