Repository: sendgrid/sendgrid-python

Issue:
Support for text/x-amp-html mime type
### Issue Summary - Feature Request
I am trying to use the SendGrid python client to send AMP dynamic emails. AMP emails require the `text/x-amp-html` content type [as per SendGrid's doc](https://amp.dev/documentation/guides-and-tutorials/learn/email-spec/amp-email-structure/?format=websites)
However the python library [does not support](https://github.com/sendgrid/sendgrid-python/blob/main/sendgrid/helpers/mail/mime_type.py) the `text/x-amp-html` mime type yet. 


Discussion:
modernwarfareuplink: @thinkingserious need your superpowers here !!! . Please check and let me know about the PR. Thanks
modernwarfareuplink: @thinkingserious gentle reminder. Please let me know if you need any help. Thanks

modernwarfareuplink: Thanks for the review @thinkingserious. Will update in 2 days, caught up with something. Will let you know, once the changes are done.
thinkingserious: That sounds good @modernwarfareuplink, thank you!
modernwarfareuplink: Thanks for the review. I am really sorry that it took this long and no updates from my side. Will finish adding the tests by morning (in around 12 hours) and let you know.
Thank you and happy new year @thinkingserious 
modernwarfareuplink: @thinkingserious  have resolved your comments, made few changes around the ordering and added appropriate tests. Also found that the build was failing due to copyrights year and fixed it too. Please review and let me know if you need any additional details. Thank you 
modernwarfareuplink: @thinkingserious gentle reminder for review. Please let me know if you have any issues. Thank you
modernwarfareuplink: Have updated the minor comment @JenniferMah . Please do have a look once. Thank you
modernwarfareuplink: @thinkingserious need your superpowers for review and merging the changes 🙏 

Patch:
diff --git a/sendgrid/helpers/mail/__init__.py b/sendgrid/helpers/mail/__init__.py
index 15cc1cc..28d80ac 100644
--- a/sendgrid/helpers/mail/__init__.py
+++ b/sendgrid/helpers/mail/__init__.py
@@ -27,6 +27,7 @@ from .group_id import GroupId
 from .groups_to_display import GroupsToDisplay
 from .header import Header
 from .html_content import HtmlContent
+from .amp_html_content import AmpHtmlContent
 from .ip_pool_name import IpPoolName
 from .mail_settings import MailSettings
 from .mail import Mail
diff --git a/sendgrid/helpers/mail/amp_html_content.py b/sendgrid/helpers/mail/amp_html_content.py
new file mode 100644
index 0000000..1a28205
--- /dev/null
+++ b/sendgrid/helpers/mail/amp_html_content.py
@@ -0,0 +1,59 @@
+from .content import Content
+from .validators import ValidateApiKey
+
+
+class AmpHtmlContent(Content):
+    """AMP HTML content to be included in your email."""
+
+    def __init__(self, content):
+        """Create an AMP HTML Content with the specified MIME type and content.
+
+        :param content: The AMP HTML content.
+        :type content: string
+        """
+        self._content = None
+        self._validator = ValidateApiKey()
+
+        if content is not None:
+            self.content = content
+
+    @property
+    def mime_type(self):
+        """The MIME type for AMP HTML content.
+
+        :rtype: string
+        """
+        return "text/x-amp-html"
+
+    @property
+    def content(self):
+        """The actual AMP HTML content.
+
+        :rtype: string
+        """
+        return self._content
+
+    @content.setter
+    def content(self, value):
+        """The actual AMP HTML content.
+
+        :param value: The actual AMP HTML content.
+        :type value: string
+        """
+        self._validator.validate_message_dict(value)
+        self._content = value
+
+    def get(self):
+        """
+        Get a JSON-ready representation of this AmpContent.
+
+        :returns: This AmpContent, ready for use in a request body.
+        :rtype: dict
+        """
+        content = {}
+        if self.mime_type is not None:
+            content["type"] = self.mime_type
+
+        if self.content is not None:
+            content["value"] = self.content
+        return content
diff --git a/sendgrid/helpers/mail/content.py b/sendgrid/helpers/mail/content.py
index 73bf9f6..618eee9 100644
--- a/sendgrid/helpers/mail/content.py
+++ b/sendgrid/helpers/mail/content.py
@@ -29,7 +29,7 @@ class Content(object):
     @property
     def mime_type(self):
         """The MIME type of the content you are including in your email.
-        For example, "text/plain" or "text/html".
+        For example, "text/plain" or "text/html" or "text/x-amp-html".
 
         :rtype: string
         """
@@ -38,11 +38,11 @@ class Content(object):
     @mime_type.setter
     def mime_type(self, value):
         """The MIME type of the content you are including in your email.
-        For example, "text/plain" or "text/html".
+        For example, "text/plain" or "text/html" or "text/x-amp-html".
 
         :param value: The MIME type of the content you are including in your
                       email.
-        For example, "text/plain" or "text/html".
+        For example, "text/plain" or "text/html" or "text/x-amp-html".
         :type value: string
         """
         self._mime_type = value
diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py
index db23993..0069a3f 100644
--- a/sendgrid/helpers/mail/mail.py
+++ b/sendgrid/helpers/mail/mail.py
@@ -27,6 +27,7 @@ class Mail(object):
             subject=None,
             plain_text_content=None,
             html_content=None,
+            amp_html_content=None,
             global_substitutions=None,
             is_multiple=False):
         """
@@ -43,6 +44,8 @@ class Mail(object):
         :type plain_text_content: string, optional
         :param html_content: The html body of the email
         :type html_content: string, optional
+        :param amp_html_content: The amp-html body of the email
+        :type amp_html_content: string, optional
         """
         self._attachments = None
         self._categories = None
@@ -71,6 +74,8 @@ class Mail(object):
             self.subject = subject
         if plain_text_content is not None:
             self.add_content(plain_text_content, MimeType.text)
+        if amp_html_content is not None:
+            self.add_content(amp_html_content, MimeType.amp)
         if html_content is not None:
             self.add_content(html_content, MimeType.html)
 
@@ -725,9 +730,23 @@ class Mail(object):
         """
         if isinstance(content, str):
             content = Content(mime_type, content)
-        # Content of mime type text/plain must always come first
-        if content.mime_type == "text/plain":
+        # Content of mime type text/plain must always come first, followed by text/x-amp-html and then text/html
+        if content.mime_type == MimeType.text:
             self._contents = self._ensure_insert(content, self._contents)
+        elif content.mime_type == MimeType.amp:
+            if self._contents:
+                for _content in self._contents:
+                    # this is written in the context that plain text content will always come earlier than the html content
+                    if _content.mime_type == MimeType.text:
+                        index = 1
+                        break
+                    elif _content.mime_type == MimeType.html:
+                        index = 0
+                        break
+            else:
+                index = 0
+            self._contents = self._ensure_append(
+                content, self._contents, index=index)
         else:
             if self._contents:
                 index = len(self._contents)
diff --git a/sendgrid/helpers/mail/mime_type.py b/sendgrid/helpers/mail/mime_type.py
index 0d0c9b3..a2f88c5 100644
--- a/sendgrid/helpers/mail/mime_type.py
+++ b/sendgrid/helpers/mail/mime_type.py
@@ -3,3 +3,4 @@ class MimeType(object):
     """
     text = "text/plain"
     html = "text/html"
+    amp = "text/x-amp-html"
diff --git a/use_cases/sending_amp_html_content.md b/use_cases/sending_amp_html_content.md
new file mode 100644
index 0000000..616b520
--- /dev/null
+++ b/use_cases/sending_amp_html_content.md
@@ -0,0 +1,102 @@
+# Sending AMP-HTML Email
+
+Following is an example on how to send an AMP HTML Email.
+Currently, we require AMP HTML and any one of HTML or Plain Text content (preferrably both) for improved deliverability or fallback for AMP HTML Email for supporting older clients and showing alternate content after 30 days.
+
+For more information on AMP emails pls check the [official AMP email page](https://amp.dev/about/email/)
+
+```python
+import os
+from sendgrid import SendGridAPIClient
+from sendgrid.helpers.mail import Mail
+
+# The below amp html email is taken from [Google AMP Hello World Email](https://amp.dev/documentation/examples/introduction/hello_world_email/)
+amp_html_content = '''
+<!--
+## Introduction
+
+Learn how to create your first AMP email. This sample covers the basic structure of AMP emails.
+-->
+
+<!-- -->
+<!-- Doctype declaration is required. -->
+<!doctype html>
+<!-- This tells everyone that this is an AMP email. `<html amp4email>` works too. -->
+<html ⚡4email>
+<!-- ## Head -->
+<!-- -->
+<head>
+  <!-- The charset definition must be the first child of the `<head>` tag. -->
+  <meta charset="utf-8">
+  <!-- The AMP runtime.-->
+  <script async src="https://cdn.ampproject.org/v0.js"></script>
+  <!-- The AMP for Email boilerplate.  -->
+  <style amp4email-boilerplate>body{visibility:hidden}</style>
+  <!-- Import AMP components in the header, for example the `amp-carousel` component. -->
+  <script async custom-element="amp-carousel" src="https://cdn.ampproject.org/v0/amp-carousel-0.1.js"></script>
+  <!--
+    ### Allowed CSS properties and selectors
+
+    All CSS in any AMP document must be included in a `<style amp-custom>` tag within the header as shown above. Inline style attributes are not allowed in AMP.  CSS allowed within email messages vary depending on the email provider. For reference, the list of CSS properties and values allowed in Gmail can be found at [Gmail Supported CSS Properties & Media Queries](https://developers.google.com/gmail/design/reference/supported_css).
+  -->
+  <style amp-custom>
+    .emailbody {
+      padding: 16px;
+    }
+    .helloworld {
+      font-family: Helvetica;
+      color: red;
+      font-size: 24px;
+      padding-bottom: 8px;
+    }
+    .images {
+      max-width: 100%;
+    }
+  </style>
+  <!--
+    Note: The entire `<style>` tag cannot exceed 75,000 bytes. The validator will check for this.
+  -->
+<!-- -->
+</head>
+<!-- ## Body -->
+<!-- -->
+<body>
+  <div class="emailbody">
+    <!--
+      Just like in AMP web pages, most HTML tags can be used directly.
+    -->
+    <h1 class="helloworld">Hello!</h1>
+    <!--
+      Certain tags, such as the `<img>` tag, are replaced with equivalent or slightly enhanced custom AMP HTML tags (see [HTML Tags in the specification](https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md)).
+    -->
+    <amp-img src="https://amp.dev/static/samples/img/amp.jpg" width="800" height="600" layout="responsive"></amp-img>
+    <!--
+    Important: URLs most use absolute paths in AMP for Email.
+
+    Since we imported the `amp-carousel` component in the header as example, let's try it here.
+    -->
+    <amp-carousel width="800" height="600" layout="responsive" type="slides" controls>
+      <amp-img src="https://amp.dev/static/samples/img/image1.jpg" width="800" height="600" alt="a sample image"></amp-img>
+      <amp-img src="https://amp.dev/static/samples/img/image2.jpg" width="800" height="600" alt="another sample image"></amp-img>
+      <amp-img src="https://amp.dev/static/samples/img/image3.jpg" width="800" height="600" alt="and another sample image"></amp-img>
+    </amp-carousel>
+  </div>
+</body>
+</html>
+'''
+
+message = Mail(
+    from_email='example@example.com',
+    to_emails='example@example.com',
+    subject='Sending with Twilio SendGrid is Fun',
+    html_content='<strong>and easy to do anywhere, even with Python</strong>',
+    amp_html_content=amp_html_content)
+try:
+    sg =  SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY'))
+    response = sg.send(message)
+    print(response.status_code)
+    print(response.body)
+    print(response.headers)
+except Exception as e:
+    print(e.message)
+```
\ No newline at end of file


Test patch:
diff --git a/test/test_mail_helpers.py b/test/test_mail_helpers.py
index 202d394..d0d09c9 100644
--- a/test/test_mail_helpers.py
+++ b/test/test_mail_helpers.py
@@ -17,6 +17,40 @@ from sendgrid.helpers.mail import (
     Subject, Substitution, To, Cc, Bcc, TrackingSettings
 )
 
+# The below amp html email content is taken from [Google AMP Hello World Email](https://amp.dev/documentation/examples/introduction/hello_world_email/)
+amp_html_content = '''<!doctype html><html amp4email><head><meta charset="utf-8"><script async src="https://cdn.ampproject.org/v0.js"></script><style amp4email-boilerplate>body{visibility:hidden}</style><script async custom-element="amp-carousel" src="https://cdn.ampproject.org/v0/amp-carousel-0.1.js"></script><style amp-custom>.emailbody {padding: 16px;}.helloworld {font-family: Helvetica;color: red;font-size: 24px;padding-bottom: 8px;}.images {max-width: 100%;}</style></head><body><div class="emailbody"><h1 class="helloworld">Hello!</h1><amp-img src="https://amp.dev/static/samples/img/amp.jpg" width="800" height="600" layout="responsive"></amp-img></div></body></html>'''
+
+response_content_with_all_three_mime_contents = json.dumps({
+    "content": [
+        {
+            "type": "text/plain",
+            "value": "and easy to do anywhere, even with Python"
+        },
+        {
+            "type": "text/x-amp-html",
+            "value": amp_html_content
+        },
+        {
+            "type": "text/html",
+            "value": "<strong>and easy to do anywhere, even with Python</strong>"
+        }
+    ],
+    "from": {
+        "email": "test+from@example.com",
+        "name": "Example From Name"
+    },
+    "personalizations": [
+        {
+            "to": [
+                {
+                    "email": "test+to@example.com",
+                    "name": "Example To Name"
+                }
+            ]
+        }
+    ],
+    "subject": "Sending with SendGrid is Fun"
+})
 
 class UnitTests(unittest.TestCase):
 
@@ -284,6 +318,239 @@ class UnitTests(unittest.TestCase):
             }''')
         )
 
+    def test_single_email_with_all_three_email_contents_to_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun'),
+            plain_text_content=PlainTextContent(
+                'and easy to do anywhere, even with Python'),
+            amp_html_content=AmpHtmlContent(amp_html_content),
+            html_content=HtmlContent(
+                '<strong>and easy to do anywhere, even with Python</strong>')
+        )
+
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content_with_all_three_mime_contents)
+        )
+
+    def test_single_email_with_amp_and_html_contents_to_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun'),
+            amp_html_content=AmpHtmlContent(amp_html_content),
+            html_content=HtmlContent(
+                '<strong>and easy to do anywhere, even with Python</strong>')
+        )
+
+        response_content = json.dumps({
+            "content": [
+                {
+                    "type": "text/x-amp-html",
+                    "value": amp_html_content
+                },
+                {
+                    "type": "text/html",
+                    "value": "<strong>and easy to do anywhere, even with Python</strong>"
+                }
+            ],
+            "from": {
+                "email": "test+from@example.com",
+                "name": "Example From Name"
+            },
+            "personalizations": [
+                {
+                    "to": [
+                        {
+                            "email": "test+to@example.com",
+                            "name": "Example To Name"
+                        }
+                    ]
+                }
+            ],
+            "subject": "Sending with SendGrid is Fun"
+        })
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content)
+        )
+
+    def test_single_email_with_amp_and_plain_contents_to_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun'),
+            plain_text_content=PlainTextContent(
+                'and easy to do anywhere, even with Python'),
+            amp_html_content=AmpHtmlContent(amp_html_content)
+        )
+
+        response_content = json.dumps({
+            "content": [
+                {
+                    "type": "text/plain",
+                    "value": "and easy to do anywhere, even with Python"
+                },
+                {
+                    "type": "text/x-amp-html",
+                    "value": amp_html_content
+                }
+            ],
+            "from": {
+                "email": "test+from@example.com",
+                "name": "Example From Name"
+            },
+            "personalizations": [
+                {
+                    "to": [
+                        {
+                            "email": "test+to@example.com",
+                            "name": "Example To Name"
+                        }
+                    ]
+                }
+            ],
+            "subject": "Sending with SendGrid is Fun"
+        })
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content)
+        )
+
+    ## Check ordering of MIME types in different variants - start
+    def test_single_email_with_all_three_contents_in_collapsed_order_of_plain_amp_html_content_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun')
+        )
+        message.content = PlainTextContent(
+            'and easy to do anywhere, even with Python')
+        message.content = AmpHtmlContent(amp_html_content)
+        message.content = HtmlContent(
+            '<strong>and easy to do anywhere, even with Python</strong>')
+
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content_with_all_three_mime_contents)
+        )
+
+    def test_single_email_with_all_three_contents_in_collapsed_order_of_plain_html_amp_content_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun')
+        )
+        message.content = PlainTextContent(
+            'and easy to do anywhere, even with Python')
+        message.content = HtmlContent(
+            '<strong>and easy to do anywhere, even with Python</strong>')
+        message.content = AmpHtmlContent(amp_html_content)
+
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content_with_all_three_mime_contents)
+        )
+
+    def test_single_email_with_all_three_contents_in_collapsed_order_of_html_plain_amp_content_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun')
+        )
+        message.content = HtmlContent(
+            '<strong>and easy to do anywhere, even with Python</strong>')
+        message.content = PlainTextContent(
+            'and easy to do anywhere, even with Python')
+        message.content = AmpHtmlContent(amp_html_content)
+
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content_with_all_three_mime_contents)
+        )
+
+    def test_single_email_with_all_three_contents_in_collapsed_order_of_html_amp_plain_content_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun')
+        )
+        message.content = HtmlContent(
+            '<strong>and easy to do anywhere, even with Python</strong>')
+        message.content = AmpHtmlContent(amp_html_content)
+        message.content = PlainTextContent(
+            'and easy to do anywhere, even with Python')
+
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content_with_all_three_mime_contents)
+        )
+
+    def test_single_email_with_all_three_contents_in_collapsed_order_of_amp_html_plain_content_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun')
+        )
+        message.content = AmpHtmlContent(amp_html_content)
+        message.content = HtmlContent(
+            '<strong>and easy to do anywhere, even with Python</strong>')
+        message.content = PlainTextContent(
+            'and easy to do anywhere, even with Python')
+
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content_with_all_three_mime_contents)
+        )
+
+    def test_single_email_with_all_three_contents_in_collapsed_order_of_amp_plain_html_content_single_recipient(self):
+        from sendgrid.helpers.mail import (Mail, From, To, Subject,
+                                           PlainTextContent, HtmlContent, AmpHtmlContent)
+        self.maxDiff = None
+        message = Mail(
+            from_email=From('test+from@example.com', 'Example From Name'),
+            to_emails=To('test+to@example.com', 'Example To Name'),
+            subject=Subject('Sending with SendGrid is Fun')
+        )
+        message.content = AmpHtmlContent(amp_html_content)
+        message.content = PlainTextContent(
+            'and easy to do anywhere, even with Python')
+        message.content = HtmlContent(
+            '<strong>and easy to do anywhere, even with Python</strong>')
+
+        self.assertEqual(
+            message.get(),
+            json.loads(response_content_with_all_three_mime_contents)
+        )
+
+    ## end
+
     def test_value_error_is_raised_on_to_emails_set_to_list_of_lists(self):
         from sendgrid.helpers.mail import (PlainTextContent, HtmlContent)
         self.maxDiff = None