Repository: Stranger6667/postmarker
Pull request #68 (2016-10-29 09:59:14)

Inline images

codecov-io: ## [Current coverage](https://codecov.io/gh/Stranger6667/postmarker/pull/68?src=pr) is 100% (diff: 100%)
> Merging [#68](https://codecov.io/gh/Stranger6667/postmarker/pull/68?src=pr) into [master](https://codecov.io/gh/Stranger6667/postmarker/branch/master?src=pr) will not change coverage

```diff
@@           master   #68   diff @@
===================================
  Files          13    13          
  Lines         449   460    +11   
  Methods         0     0          
  Messages        0     0          
  Branches       46    50     +4   
===================================
+ Hits          449   460    +11   
  Misses          0     0          
  Partials        0     0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [756455d...dc54d57](https://codecov.io/gh/Stranger6667/postmarker/compare/756455d683f766b7d3a8eab246255e5b3cebe6ca...dc54d57f442634b1005297a6c13232fefe9cad11?src=pr)

Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 09b20e5..7f7b7b4 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -9,6 +9,7 @@ Changelog
 - Support for link tracking. `#62`_
 - Better exceptions handling. `#50`_
 - Support for spam check API. `#57`_
+- Inline images support. `#52`_
 
 0.5.3 - 27.10.2016
 ------------------
@@ -82,6 +83,7 @@ Changelog
 .. _#59: https://github.com/Stranger6667/postmarker/issues/59
 .. _#57: https://github.com/Stranger6667/postmarker/issues/57
 .. _#55: https://github.com/Stranger6667/postmarker/issues/55
+.. _#52: https://github.com/Stranger6667/postmarker/issues/52
 .. _#51: https://github.com/Stranger6667/postmarker/issues/51
 .. _#50: https://github.com/Stranger6667/postmarker/issues/50
 .. _#46: https://github.com/Stranger6667/postmarker/issues/46
diff --git a/docs/emails.rst b/docs/emails.rst
index f40a452..db9a96a 100644
--- a/docs/emails.rst
+++ b/docs/emails.rst
@@ -84,6 +84,24 @@ Then pass the attachments to :py:meth:`~postmarker.models.emails.EmailManager.se
         Attachments=[msg_1, msg_2, msg_3, msg_4]
     )
 
+Postmarker also supports inline images. Here are the possible usage options:
+
+.. code-block:: python
+
+    >>> tuple_image = ('image', 'content', 'image/png', 'cid:image@example.com')
+    >>> mime = MIMEImage('test content', 'png', name='image1.png')
+    >>> mime.add_header('Content-ID', '<image1@example.com>')
+    >>> mime_inline = MIMEImage('test content', 'png', name='image3.png')
+    >>> mime_inline.add_header('Content-ID', '<image3@example.com>')
+    >>> mime_inline.add_header('Content-Disposition', 'inline', filename='image3.png')
+    >>> postmark.emails.send(
+        From='sender@example.com',
+        To='receiver@example.com',
+        Subject='Postmark test',
+        HtmlBody='<html><body><strong>Hello</strong> dear Postmark user.</body></html>',
+        Attachments=[tuple_image, mime, mime_inline]
+    )
+
 To send email in a batch there is :py:meth:`~postmarker.models.emails.EmailManager.send_batch` method.
 
 .. code-block:: python
diff --git a/postmarker/models/emails.py b/postmarker/models/emails.py
index b22481c..281fec0 100644
--- a/postmarker/models/emails.py
+++ b/postmarker/models/emails.py
@@ -42,28 +42,39 @@ def prepare_attachments(attachment):
     Converts incoming attachment into dictionary.
     """
     if isinstance(attachment, tuple):
-        attachment = {
+        result = {
             'Name': attachment[0],
             'Content': attachment[1],
             'ContentType': attachment[2],
         }
+        if len(attachment) == 4:
+            result['ContentID'] = attachment[3]
     elif isinstance(attachment, MIMEBase):
-        attachment = {
+        result = {
             'Name': attachment.get_filename(),
             'Content': attachment.get_payload(),
             'ContentType': attachment.get_content_type(),
         }
+        content_id = attachment.get('Content-ID')
+        if content_id:
+            if content_id.startswith('<') and content_id.endswith('>'):
+                content_id = content_id[1:-1]
+            if (attachment.get('Content-Disposition') or '').startswith('inline'):
+                content_id = 'cid:%s' % content_id
+            result['ContentID'] = content_id
     elif isinstance(attachment, str):
         content_type = guess_content_type(attachment)
         filename = os.path.basename(attachment)
         with open(attachment, 'rb') as fd:
             data = fd.read()
-        attachment = {
+        result = {
             'Name': filename,
             'Content': b64encode(data).decode('utf-8'),
             'ContentType': content_type
         }
-    return attachment
+    else:
+        result = attachment
+    return result
 
 
 def deconstruct_multipart(message):


Test patch:
diff --git a/tests/models/test_emails.py b/tests/models/test_emails.py
index eee9205..f70d125 100644
--- a/tests/models/test_emails.py
+++ b/tests/models/test_emails.py
@@ -3,6 +3,7 @@ import os
 import sys
 from email import encoders
 from email.mime.base import MIMEBase
+from email.mime.image import MIMEImage
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 
@@ -69,6 +70,15 @@ else:
     ENCODED_CONTENT = 'dGVzdCBjb250ZW50\n'
 
 
+IMAGE = MIMEImage(b'test content', 'png', name='image1.png')
+IMAGE.add_header('Content-ID', '<image1@example.com>')
+IMAGE_NO_BRACKETS = MIMEImage(b'test content', 'png', name='image2.png')
+IMAGE_NO_BRACKETS.add_header('Content-ID', 'image2@example.com')
+INLINE_IMAGE = MIMEImage(b'test content', 'png', name='image3.png')
+INLINE_IMAGE.add_header('Content-ID', '<image3@example.com>')
+INLINE_IMAGE.add_header('Content-Disposition', 'inline', filename='image3.png')
+
+
 class TestSimpleSend:
 
     @pytest.fixture
@@ -280,3 +290,46 @@ class TestEmail:
         assert email.Cc == MIME_MESSAGE['Cc']
         assert email.Bcc == MIME_MESSAGE['Bcc']
         assert email.ReplyTo == MIME_MESSAGE['Reply-To']
+
+    @pytest.mark.parametrize('image, expected', (
+        (
+            ('image', 'content', 'image/png', 'cid:image@example.com'),
+            {
+                'Name': 'image',
+                'Content': 'content',
+                'ContentType': 'image/png',
+                'ContentID': 'cid:image@example.com'
+            },
+        ),
+        (
+            IMAGE,
+            {
+                'Name': 'image1.png',
+                'Content': ENCODED_CONTENT,
+                'ContentType': 'image/png',
+                'ContentID': 'image1@example.com'
+            }
+        ),
+        (
+            IMAGE_NO_BRACKETS,
+            {
+                'Name': 'image2.png',
+                'Content': ENCODED_CONTENT,
+                'ContentType': 'image/png',
+                'ContentID': 'image2@example.com'
+            }
+        ),
+        (
+            INLINE_IMAGE,
+            {
+                'Name': 'image3.png',
+                'Content': ENCODED_CONTENT,
+                'ContentType': 'image/png',
+                'ContentID': 'cid:image3@example.com'
+            }
+        ),
+    ))
+    def test_inline_image(self, email, patched_request, image, expected):
+        email.attach(image)
+        email.send()
+        assert patched_request.call_args[1]['json']['Attachments'] == [expected]