From 8ac7f77844fd2d23802428a5958f52e34e5ee659 Mon Sep 17 00:00:00 2001 From: Richard Huang Date: Sun, 20 Dec 2015 15:40:57 -0800 Subject: [PATCH] Minor enhancements. - Add subject and text filters - Add non-secure connection support - Adjust documentation - Add more unit test - Add backward compatible support - Add `Delete All Emails`, `Delete Email`, `Mark All Emails As Read`, and `Mark Email As Read` keywords. - Add alternative keyword to deprecated keywords. --- CHANGELOG.rst | 11 + README.rst | 85 ++++--- doc/ImapLibrary.html | 2 +- src/ImapLibrary/__init__.py | 395 +++++++++++++++++++++------------ src/ImapLibrary/version.py | 2 +- test/utest/test_imaplibrary.py | 242 +++++++++++++++++++- 6 files changed, 541 insertions(+), 196 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 287920e..c1e90cb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,14 @@ +0.2.1 (2015.12.20) +================== + +* Add subject and text filters +* Add non-secure connection support +* Adjust documentation +* Add more unit test +* Add backward compatible support +* Add `Delete All Emails`, `Delete Email`, `Mark All Emails As Read`, and `Mark Email As Read` keywords +* Add alternative keyword to deprecated keywords + 0.2.0 (2015.12.15) ================== diff --git a/README.rst b/README.rst index d5b9ada..4f39769 100644 --- a/README.rst +++ b/README.rst @@ -10,18 +10,11 @@ ImapLibrary is a IMAP email testing library for `Robot Framework`_. More information about this library can be found in the `Keyword Documentation`_. -Non-Backward Compatible Warning -------------------------------- - -There are inevitable changes to parameter names that would not be backward compatible with -release 0.1.4 downwards. -These changes are made to comply with Python code style guide on `Method Names and Instance Variables`_. - Authoritative Repository ------------------------ -This repository is the new authoritative repository for robotframework-imaplibrary package, -and I am also the new project maintainer for robotframework-imaplibrary project. +This repository is the new authoritative repository for `robotframework-imaplibrary`_ package, +and I am also the new project maintainer for `robotframework-imaplibrary`_ project. I will go through the pull requests from old repository, as well as issue list. I will try to accomodate as much as I could as time permit. **There is no need to re-post.** @@ -31,46 +24,46 @@ If you are interested to contribute back to this project, please see **Contribut Example ''''''' -+----------------+----------------------------+-------------------------------+-----------------+ -| Open Mailbox | server=imap.googlemail.com | user=email@gmail.com | password=secret | -+----------------+----------------------------+-------------------------------+-----------------+ -| ${LATEST} = | Wait For Mail | from_email=noreply@domain.com | timeout=300 | -+----------------+----------------------------+-------------------------------+-----------------+ -| ${HTML} = | Open Link From Mail | ${LATEST} | -+----------------+----------------------------+-------------------------------------------------+ -| Should Contain | ${HTML} | Your email address has been updated | -+----------------+----------------------------+-------------------------------------------------+ -| Close Mailbox | -+-----------------------------------------------------------------------------------------------+ ++----------------+----------------------+---------------------------+-----------------+ +| Open Mailbox | host=imap.domain.com | user=email@domain.com | password=secret | ++----------------+----------------------+---------------------------+-----------------+ +| ${LATEST} = | Wait For Email | sender=noreply@domain.com | timeout=300 | ++----------------+----------------------+---------------------------+-----------------+ +| ${HTML} = | Open Link From Email | ${LATEST} | ++----------------+----------------------+---------------------------------------------+ +| Should Contain | ${HTML} | Your email address has been updated | ++----------------+----------------------+---------------------------------------------+ +| Close Mailbox | ++-------------------------------------------------------------------------------------+ Multipart Email Example ''''''''''''''''''''''' -+----------------+----------------------------+-------------------------------+-----------------+ -| Open Mailbox | server=imap.googlemail.com | user=email@gmail.com | password=secret | -+----------------+----------------------------+-------------------------------+-----------------+ -| ${LATEST} = | Wait For Mail | from_email=noreply@domain.com | timeout=300 | -+----------------+----------------------------+-------------------------------+-----------------+ -| ${parts} = | Walk Multipart Email | ${LATEST} | -+----------------+----------------------------+-------------------------------+-----------------+ -| :FOR | ${i} | IN RANGE | ${parts} | -+----------------+----------------------------+-------------------------------+-----------------+ -| \\ | Walk Multipart Email | ${LATEST} | -+----------------+----------------------------+-------------------------------------------------+ -| \\ | ${content-type} = | Get Multipart Content Type | -+----------------+----------------------------+-------------------------------------------------+ -| \\ | Continue For Loop If | '${content-type}' != 'text/html' | -+----------------+----------------------------+-------------------------------+-----------------+ -| \\ | ${payload} = | Get Multipart Payload | decode=True | -+----------------+----------------------------+-------------------------------+-----------------+ -| \\ | Should Contain | ${payload} | your email | -+----------------+----------------------------+-------------------------------+-----------------+ -| \\ | ${HTML} = | Open Link From Mail | ${LATEST} | -+----------------+----------------------------+-------------------------------+-----------------+ -| \\ | Should Contain | ${HTML} | Your email | -+----------------+----------------------------+-------------------------------+-----------------+ -| Close Mailbox | -+-----------------------------------------------------------------------------------------------+ ++----------------+----------------------+---------------------------+-----------------+ +| Open Mailbox | host=imap.domain.com | user=email@domain.com | password=secret | ++----------------+----------------------+---------------------------+-----------------+ +| ${LATEST} = | Wait For Email | sender=noreply@domain.com | timeout=300 | ++----------------+----------------------+---------------------------+-----------------+ +| ${parts} = | Walk Multipart Email | ${LATEST} | ++----------------+----------------------+---------------------------+-----------------+ +| :FOR | ${i} | IN RANGE | ${parts} | ++----------------+----------------------+---------------------------+-----------------+ +| \\ | Walk Multipart Email | ${LATEST} | ++----------------+----------------------+---------------------------------------------+ +| \\ | ${content-type} = | Get Multipart Content Type | ++----------------+----------------------+---------------------------------------------+ +| \\ | Continue For Loop If | '${content-type}' != 'text/html' | ++----------------+----------------------+---------------------------+-----------------+ +| \\ | ${payload} = | Get Multipart Payload | decode=True | ++----------------+----------------------+---------------------------+-----------------+ +| \\ | Should Contain | ${payload} | your email | ++----------------+----------------------+---------------------------+-----------------+ +| \\ | ${HTML} = | Open Link From Email | ${LATEST} | ++----------------+----------------------+---------------------------+-----------------+ +| \\ | Should Contain | ${HTML} | Your email | ++----------------+----------------------+---------------------------+-----------------+ +| Close Mailbox | ++-------------------------------------------------------------------------------------+ Installation ------------ @@ -233,12 +226,12 @@ Documentation and other similar content are provided under `Creative Commons Att .. _Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License: http://goo.gl/SNw73V .. _Imap Library CLA: https://goo.gl/forms/QMyqXJI2LM .. _Keyword Documentation: https://goo.gl/ntRuxC -.. _Method Names and Instance Variables: https://goo.gl/NxxD0n .. _pip: http://goo.gl/jlJCPE .. _Robot Framework: http://goo.gl/lES6WM .. _Robot Framework Documentation: http://goo.gl/zy53tf .. _Robot Framework installed: https://goo.gl/PFbWqM .. _Robot Framework User Guide: http://goo.gl/Q7dfPB +.. _robotframework-imaplibrary: https://goo.gl/q66LcA .. |Docs| image:: https://img.shields.io/badge/docs-latest-brightgreen.svg :target: https://goo.gl/ntRuxC :alt: Keyword Documentation diff --git a/doc/ImapLibrary.html b/doc/ImapLibrary.html index eff6bb4..16c610a 100644 --- a/doc/ImapLibrary.html +++ b/doc/ImapLibrary.html @@ -468,7 +468,7 @@ jQuery.extend({highlight:function(e,t,n,r){if(e.nodeType===3){var i=e.data.match(t);if(i){var s=document.createElement(n||"span");s.className=r||"highlight";var o=e.splitText(i.index);o.splitText(i[0].length);var u=o.cloneNode(true);s.appendChild(u);o.parentNode.replaceChild(s,o);return 1}}else if(e.nodeType===1&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&!(e.tagName===n.toUpperCase()&&e.className===r)){for(var a=0;a diff --git a/src/ImapLibrary/__init__.py b/src/ImapLibrary/__init__.py index c42e33a..b96cefb 100644 --- a/src/ImapLibrary/__init__.py +++ b/src/ImapLibrary/__init__.py @@ -19,50 +19,51 @@ IMAP Library - a IMAP email testing library. """ +from imaplib import IMAP4, IMAP4_SSL from ImapLibrary.version import get_version from re import findall -import imaplib -import time -import urllib2 +from time import sleep, time import email +import urllib2 __version__ = get_version() class ImapLibrary(object): - # pylint: disable=line-too-long """ImapLibrary is an email testing library for [http://goo.gl/lES6WM|Robot Framework]. - *Non-Backward Compatible Warning* + *Deprecated Keywords Warning* - There are inevitable changes to parameter names that would not be backward compatible with - release 0.1.4 downwards. - These changes are made to comply with Python code style guide on - [https://goo.gl/NxxD0n|Method Names and Instance Variables]. + These keywords will be removed in the future 3 to 5 releases. + | *Deprecated Keyword* | *Alternative Keyword* | + | `Open Link From Mail` | `Open Link From Email` | + | `Mark As Read` | `Mark All Emails As Read` | + | `Wait For Mail` | `Wait For Email` | Example: - | `Open Mailbox` | server=imap.googlemail.com | user=email@gmail.com | password=secret | - | ${LATEST} = | `Wait For Mail` | from_email=noreply@domain.com | timeout=300 | - | ${HTML} = | `Open Link From Mail` | ${LATEST} | | - | `Should Contain` | ${HTML} | Your email address has been updated | | - | `Close Mailbox` | | | | + | `Open Mailbox` | host=imap.domain.com | user=email@domain.com | password=secret | + | ${LATEST} = | `Wait For Email` | sender=noreply@domain.com | timeout=300 | + | ${HTML} = | `Open Link From Email` | ${LATEST} | | + | `Should Contain` | ${HTML} | address has been updated | | + | `Close Mailbox` | | | | Multipart Email Example: - | `Open Mailbox` | server=imap.googlemail.com | user=email@gmail.com | password=secret | - | ${LATEST} = | `Wait For Mail` | from_email=noreply@domain.com | timeout=300 | - | ${parts} = | `Walk Multipart Email` | ${LATEST} | | - | :FOR | ${i} | IN RANGE | ${parts} | - | \\ | `Walk Multipart Email` | ${LATEST} | | - | \\ | ${content-type} = | `Get Multipart Content Type` | | - | \\ | `Continue For Loop If` | '${content-type}' != 'text/html' | | - | \\ | ${payload} = | `Get Multipart Payload` | decode=True | - | \\ | `Should Contain` | ${payload} | your email | - | \\ | ${HTML} = | `Open Link From Mail` | ${LATEST} | - | \\ | `Should Contain` | ${HTML} | Your email | - | `Close Mailbox` | | | | + | `Open Mailbox` | host=imap.domain.com | user=email@domain.com | password=secret | + | ${LATEST} = | `Wait For Email` | sender=noreply@domain.com | timeout=300 | + | ${parts} = | `Walk Multipart Email` | ${LATEST} | | + | :FOR | ${i} | IN RANGE | ${parts} | + | \\ | `Walk Multipart Email` | ${LATEST} | | + | \\ | ${ctype} = | `Get Multipart Content Type` | | + | \\ | `Continue For Loop If` | '${ctype}' != 'text/html' | | + | \\ | ${payload} = | `Get Multipart Payload` | decode=True | + | \\ | `Should Contain` | ${payload} | your email | + | \\ | ${HTML} = | `Open Link From Email` | ${LATEST} | + | \\ | `Should Contain` | ${HTML} | Your email | + | `Close Mailbox` | | | | """ - # pylint: disable=line-too-long + PORT = 143 + PORT_SECURE = 993 ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = __version__ @@ -79,61 +80,161 @@ def __init__(self): self._mp_iter = None self._mp_msg = None self._part = None - self._port = 993 - def open_mailbox(self, server, user, password): - """Open the mailbox on a mail server with a valid authentication. + def close_mailbox(self): + """Close IMAP email client session. + + Examples: + | Close Mailbox | """ - self._imap = imaplib.IMAP4_SSL(server, self._port) - self._imap.login(user, password) - self._imap.select() - self._init_multipart_walk() + self._imap.close() + + def delete_all_emails(self): + """Delete all emails. - def wait_for_mail(self, from_email=None, to_email=None, status=None, timeout=60): + Examples: + | Delete All Emails | """ - Wait for an incoming mail from a specific sender to - a specific mail receiver. Check the mailbox every 10 - seconds for incoming mails until the timeout is exceeded. - Returns the mail number of the latest email received. + for mail in self._mails: + self._imap.store(mail, '+FLAGS', r'\DELETED') + self._imap.expunge() + + def delete_email(self, email_index): + """Delete email on given ``email_index``. - ``status`` is a mailbox status filter. - Please see [https://goo.gl/3KKHoY|Mailbox Status] for more information. + Arguments: + - ``email_index``: An email index to identity the email message. - `timeout` sets the maximum waiting time until an error is raised. + Examples: + | Delete Email | INDEX | """ - end_time = time.time() + int(timeout) - while time.time() < end_time: - self._mails = self._check_emails(from_email, to_email, status) - if len(self._mails) > 0: - return self._mails[-1] - if time.time() < end_time: - time.sleep(10) - raise AssertionError("No mail received within time") + self._imap.store(email_index, '+FLAGS', r'\DELETED') + self._imap.expunge() + + def get_email_body(self, email_index): + """Returns the decoded email body on multipart email message, + otherwise returns the body text. + + Arguments: + - ``email_index``: An email index to identity the email message. + + Examples: + | Get Email Body | INDEX | + """ + if self._is_walking_multipart(email_index): + body = self.get_multipart_payload(decode=True) + else: + body = self._imap.fetch(email_index, '(BODY[TEXT])')[1][0][1].decode('quoted-printable') + return body def get_links_from_email(self, email_index): - ''' - Finds all links in an email body and returns them + """Returns all links found in the email body from given ``email_index``. - `email_index` is the index number of the mail to open - ''' + Arguments: + - ``email_index``: An email index to identity the email message. + + Examples: + | Get Links From Email | INDEX | + """ body = self.get_email_body(email_index) return findall(r'href=[\'"]?([^\'" >]+)', body) - def get_matches_from_email(self, email_index, regexp): - """ - Finds all occurrences of a regular expression + def get_matches_from_email(self, email_index, pattern): + """Returns all Regular Expression ``pattern`` found in the email body + from given ``email_index``. + + Arguments: + - ``email_index``: An email index to identity the email message. + - ``pattern``: It consists of one or more character literals, operators, or constructs. + + Examples: + | Get Matches From Email | INDEX | PATTERN | """ body = self.get_email_body(email_index) - return findall(regexp, body) + return findall(pattern, body) - def open_link_from_mail(self, email_index, link_index=0): + def get_multipart_content_type(self): + """Returns the content type of current part of selected multipart email message. + + Examples: + | Get Multipart Content Type | + """ + return self._part.get_content_type() + + def get_multipart_field(self, field): + """Returns the value of given header ``field`` name. + + Arguments: + - ``field``: A header field name: ``From``, ``To``, ``Subject``, ``Date``, etc. + All available header field names of an email message can be found by running + `Get Multipart Field Names` keyword. + + Examples: + | Get Multipart Field | Subject | """ - Find a link in an email body and open the link. - Returns the link's html. + return self._mp_msg[field] - `email_index` is the index number of the mail to open - `link_index` declares which link shall be opened (link - index in body text) + def get_multipart_field_names(self): + """Returns all available header field names of selected multipart email message. + + Examples: + | Get Multipart Field Names | + """ + return list(self._mp_msg.keys()) + + def get_multipart_payload(self, decode=False): + """Returns the payload of current part of selected multipart email message. + + Arguments: + - ``decode``: An indicator flag to decode the email message. (Default False) + + Examples: + | Get Multipart Payload | + | Get Multipart Payload | decode=True | + """ + payload = self._part.get_payload(decode=decode) + charset = self._part.get_content_charset() + if charset is not None: + return payload.decode(charset) + return payload + + def mark_all_emails_as_read(self): + """Mark all received emails as read. + + Examples: + | Mark All Emails As Read | + """ + for mail in self._mails: + self._imap.store(mail, '+FLAGS', r'\SEEN') + + def mark_as_read(self): + """****DEPRECATED**** + Shortcut to `Mark All Emails As Read`. + """ + self.mark_all_emails_as_read() + + def mark_email_as_read(self, email_index): + """Mark email on given ``email_index`` as read. + + Arguments: + - ``email_index``: An email index to identity the email message. + + Examples: + | Mark Email As Read | INDEX | + """ + self._imap.store(email_index, '+FLAGS', r'\SEEN') + + def open_link_from_email(self, email_index, link_index=0): + """Open link URL from given ``link_index`` in email message body of given ``email_index``. + Returns HTML content of opened link URL. + + Arguments: + - ``email_index``: An email index to identity the email message. + - ``link_index``: The link index to be open. (Default 0) + + Examples: + | Open Link From Email | + | Open Link From Email | 1 | """ urls = self.get_links_from_email(email_index) @@ -148,117 +249,129 @@ def open_link_from_mail(self, email_index, link_index=0): else: raise AssertionError("Link number %i not found!" % link_index) - def delete_email(self, email_index): - """ - Delete the selected email. + def open_link_from_mail(self, email_index, link_index=0): + """****DEPRECATED**** + Shortcut to `Open Link From Email`. """ - self._imap.store(email_index, '+FLAGS', '\\Deleted') - self._imap.expunge() + return self.open_link_from_email(email_index, link_index) - def close_mailbox(self): - """ - Close the mailbox after finishing all mail activities of a user. - """ - self._imap.close() + def open_mailbox(self, **kwargs): + """Open IMAP email client session to given ``host`` with given ``user`` and ``password``. - def mark_as_read(self): - """ - Mark all received mails as read + Arguments: + - ``host``: The IMAP host server. (Default None) + - ``is_secure``: An indicator flag to connect to IMAP host securely or not. (Default True) + - ``password``: The plaintext password to be use to authenticate mailbox on given ``host``. + - ``port``: The IMAP port number. (Default None) + - ``user``: The username to be use to authenticate mailbox on given ``host``. + + Examples: + | Open Mailbox | host=HOST | user=USER | password=SECRET | + | Open Mailbox | host=HOST | user=USER | password=SECRET | is_secure=False | + | Open Mailbox | host=HOST | user=USER | password=SECRET | port=8000 | """ - for mail in self._mails: - self._imap.store(mail, '+FLAGS', r'\SEEN') + host = kwargs.pop('host', kwargs.pop('server', None)) + is_secure = kwargs.pop('is_secure', True) + port = int(kwargs.pop('port', self.PORT_SECURE if is_secure else self.PORT)) + self._imap = IMAP4_SSL(host, port) if is_secure else IMAP4(host, port) + self._imap.login(kwargs.pop('user', None), kwargs.pop('password', None)) + self._imap.select() + self._init_multipart_walk() - def get_email_body(self, email_index): + def wait_for_email(self, **kwargs): + """Wait for email message to arrived base on any given filter criteria. + Returns email index of the latest email message received. + + Arguments: + - ``poll_frequency``: The delay value in seconds to retry the mailbox check. (Default 10) + - ``recipient``: Email recipient. (Default None) + - ``sender``: Email sender. (Default None) + - ``status``: A mailbox status filter: ``MESSAGES``, ``RECENT``, ``UIDNEXT``, + ``UIDVALIDITY``, and ``UNSEEN``. + Please see [https://goo.gl/3KKHoY|Mailbox Status] for more information. + (Default None) + - ``text``: Email body text. (Default None) + - ``timeout``: The maximum value in seconds to wait for email message to arrived. + (Default 60) + + Examples: + | Wait For Email | sender=noreply@domain.com | """ - Returns an email body + poll_frequency = float(kwargs.pop('poll_frequency', 10)) + timeout = int(kwargs.pop('timeout', 60)) + end_time = time() + timeout + while time() < end_time: + self._mails = self._check_emails(**kwargs) + if len(self._mails) > 0: + return self._mails[-1] + if time() < end_time: + sleep(poll_frequency) + raise AssertionError("No email received within %ss" % timeout) - `email_index` is the index number of the mail to open + def wait_for_mail(self, **kwargs): + """****DEPRECATED**** + Shortcut to `Wait For Email`. """ - if self._is_walking_multipart(email_index): - body = self.get_multipart_payload(decode=True) - else: - body = self._imap.fetch(email_index, '(BODY[TEXT])')[1][0][1].decode('quoted-printable') - return body + return self.wait_for_email(**kwargs) def walk_multipart_email(self, email_index): - """ - Returns the number of parts of a multipart email. Content is stored internally - to be used by other multipart keywords. Subsequent calls iterate over the - elements, and the various Get Multipart keywords retrieve their contents. + """Returns total parts of a multipart email message on given ``email_index``. + Email message is cache internally to be used by other multipart keywords: + `Get Multipart Content Type`, `Get Multipart Field`, `Get Multipart Field Names`, + `Get Multipart Field`, and `Get Multipart Payload`. + + Arguments: + - ``email_index``: An email index to identity the email message. - `email_index` is the index number of the mail to open + Examples: + | Walk Multipart Email | INDEX | """ if not self._is_walking_multipart(email_index): data = self._imap.fetch(email_index, '(RFC822)')[1][0][1] msg = email.message_from_string(data) self._start_multipart_walk(email_index, msg) - try: self._part = next(self._mp_iter) except StopIteration: self._init_multipart_walk() return False - # return number of parts return len(self._mp_msg.get_payload()) - def get_multipart_content_type(self): - """ - Return the content-type for the current part of a multipart email - """ - return self._part.get_content_type() - - def get_multipart_payload(self, decode=False): - """ - Return the payload for the current part of a multipart email - - decode is an optional flag that indicates whether to decoding - """ - payload = self._part.get_payload(decode=decode) - charset = self._part.get_content_charset() - if charset is not None: - return payload.decode(charset) - return payload - - def get_multipart_field_names(self): - """ - Return the list of header field names for the current multipart email - """ - return list(self._mp_msg.keys()) - - def get_multipart_field(self, field): - """ - Returns the content of a header field - - field is a string such as 'From', 'To', 'Subject', 'Date', etc. - """ - return self._mp_msg[field] - - def _check_emails(self, from_email, to_email, status): + def _check_emails(self, **kwargs): """Returns filtered email.""" - crit = self._criteria(from_email, to_email, status) + criteria = self._criteria(**kwargs) # Calling select before each search is necessary with gmail status, data = self._imap.select() if status != 'OK': - raise Exception('imap.select error: ' + status + ', ' + str(data)) - typ, msgnums = self._imap.search(None, *crit) + raise Exception("imap.select error: %s, %s" % (status, data)) + typ, msgnums = self._imap.search(None, *criteria) if typ != 'OK': - raise Exception('imap.search error: %s, %s, criteria=%s' % (typ, msgnums, crit)) + raise Exception('imap.search error: %s, %s, criteria=%s' % (typ, msgnums, criteria)) return msgnums[0].split() @staticmethod - def _criteria(from_email, to_email, status): + def _criteria(**kwargs): """Returns email criteria.""" - crit = [] - if from_email: - crit += ['FROM', from_email] - if to_email: - crit += ['TO', to_email] + criteria = [] + recipient = kwargs.pop('recipient', kwargs.pop('to_email', kwargs.pop('toEmail', None))) + sender = kwargs.pop('sender', kwargs.pop('from_email', kwargs.pop('fromEmail', None))) + status = kwargs.pop('status', None) + subject = kwargs.pop('subject', None) + text = kwargs.pop('text', None) + if recipient: + criteria += ['TO', recipient] + if sender: + criteria += ['FROM', sender] + if subject: + criteria += ['SUBJECT', subject] + if text: + criteria += ['TEXT', text] if status: - crit += [status] - if not crit: - crit = ['UNSEEN'] - return crit + criteria += [status] + if not criteria: + criteria = ['UNSEEN'] + return criteria def _init_multipart_walk(self): """Initialize multipart email walk.""" diff --git a/src/ImapLibrary/version.py b/src/ImapLibrary/version.py index 0cfc0ca..d52a676 100644 --- a/src/ImapLibrary/version.py +++ b/src/ImapLibrary/version.py @@ -19,7 +19,7 @@ IMAP Library - a IMAP email testing library. """ -VERSION = '0.2.0' +VERSION = '0.2.1' def get_version(): diff --git a/test/utest/test_imaplibrary.py b/test/utest/test_imaplibrary.py index 85b7a83..bc5963c 100644 --- a/test/utest/test_imaplibrary.py +++ b/test/utest/test_imaplibrary.py @@ -33,8 +33,14 @@ def setUp(self): """Instantiate the Imap library class.""" self.library = ImapLibrary() self.password = 'password' - self.secure_port = 993 + self.port = 143 + self.port_secure = 993 + self.recipient = 'my@domain.com' + self.sender = 'noreply@domain.com' self.server = 'my.imap' + self.status = 'UNSEEN' + self.subject = 'subject' + self.text = 'text' self.username = 'username' def test_should_have_default_values(self): @@ -46,12 +52,234 @@ def test_should_have_default_values(self): self.assertIsNone(self.library._mp_iter) self.assertIsNone(self.library._mp_msg) self.assertIsNone(self.library._part) - self.assertEqual(self.library._port, self.secure_port) + self.assertEqual(self.library.PORT, self.port) + self.assertEqual(self.library.PORT_SECURE, self.port_secure) - @mock.patch('ImapLibrary.imaplib.IMAP4_SSL') - def test_should_open_mailbox(self, mock_imap): - """Open mailbox should open connection to Imap server with requested credentials.""" - self.library.open_mailbox(self.server, self.username, self.password) - mock_imap.assert_called_with(self.server, self.secure_port) + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_open_secure_mailbox(self, mock_imap): + """Open mailbox should open secure connection to IMAP server with requested credentials.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + mock_imap.assert_called_with(self.server, self.port_secure) self.library._imap.login.assert_called_with(self.username, self.password) self.library._imap.select.assert_called_with() + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_open_secure_mailbox_with_custom_port(self, mock_imap): + """Open mailbox should open secure connection to IMAP server with requested credentials and custom port.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password, port=8000) + mock_imap.assert_called_with(self.server, 8000) + self.library._imap.login.assert_called_with(self.username, self.password) + self.library._imap.select.assert_called_with() + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_open_secure_mailbox_with_server_key(self, mock_imap): + """Open mailbox should open secure connection to IMAP server using 'server' key with requested credentials.""" + self.library.open_mailbox(server=self.server, user=self.username, password=self.password) + mock_imap.assert_called_with(self.server, self.port_secure) + self.library._imap.login.assert_called_with(self.username, self.password) + self.library._imap.select.assert_called_with() + + @mock.patch('ImapLibrary.IMAP4') + def test_should_open_non_secure_mailbox(self, mock_imap): + """Open mailbox should open non-secure connection to IMAP server with requested credentials.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password, is_secure=False) + mock_imap.assert_called_with(self.server, self.port) + self.library._imap.login.assert_called_with(self.username, self.password) + self.library._imap.select.assert_called_with() + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index(self, mock_imap): + """Returns email index from connected IMAP session.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['0']] + index = self.library.wait_for_email(sender=self.sender) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'FROM', self.sender) + self.assertEqual(index, '0') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index_with_sender_filter(self, mock_imap): + """Returns email index from connected IMAP session with sender filter.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['0']] + index = self.library.wait_for_email(sender=self.sender) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'FROM', self.sender) + self.assertEqual(index, '0') + index = self.library.wait_for_email(from_email=self.sender) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'FROM', self.sender) + self.assertEqual(index, '0') + index = self.library.wait_for_email(fromEmail=self.sender) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'FROM', self.sender) + self.assertEqual(index, '0') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index_with_recipient_filter(self, mock_imap): + """Returns email index from connected IMAP session with recipient filter.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['0']] + index = self.library.wait_for_email(recipient=self.recipient) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'TO', self.recipient) + self.assertEqual(index, '0') + index = self.library.wait_for_email(to_email=self.recipient) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'TO', self.recipient) + self.assertEqual(index, '0') + index = self.library.wait_for_email(toEmail=self.recipient) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'TO', self.recipient) + self.assertEqual(index, '0') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index_with_subject_filter(self, mock_imap): + """Returns email index from connected IMAP session with subject filter.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['0']] + index = self.library.wait_for_email(subject=self.subject) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'SUBJECT', self.subject) + self.assertEqual(index, '0') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index_with_text_filter(self, mock_imap): + """Returns email index from connected IMAP session with text filter.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['0']] + index = self.library.wait_for_email(text=self.text) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'TEXT', self.text) + self.assertEqual(index, '0') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index_with_status_filter(self, mock_imap): + """Returns email index from connected IMAP session with status filter.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['0']] + index = self.library.wait_for_email(status=self.status) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, self.status) + self.assertEqual(index, '0') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index_without_filter(self, mock_imap): + """Returns email index from connected IMAP session without filter.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['0']] + index = self.library.wait_for_email() + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, self.status) + self.assertEqual(index, '0') + + # DEPRECATED + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index_from_deprecated_keyword(self, mock_imap): + """Returns email index from connected IMAP session using deprecated keyword.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['0']] + index = self.library.wait_for_mail(sender=self.sender) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'FROM', self.sender) + self.assertEqual(index, '0') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_return_email_index_after_delay(self, mock_imap): + """Returns email index from connected IMAP session after some delay.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.side_effect = [['OK', ['']], ['OK', ['0']]] + index = self.library.wait_for_email(sender=self.sender, poll_frequency=0.2) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'FROM', self.sender) + self.assertEqual(index, '0') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_raise_exception_on_timeout(self, mock_imap): + """Raise exception on timeout.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['OK', ['']] + with self.assertRaises(AssertionError) as context: + self.library.wait_for_email(sender=self.sender, poll_frequency=0.2, timeout=0.3) + self.library._imap.select.assert_called_with() + self.assertTrue("No email received within 0s" in context.exception) + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_raise_exception_on_select_error(self, mock_imap): + """Raise exception on imap select error.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['NOK', ['1']] + with self.assertRaises(Exception) as context: + self.library.wait_for_email(sender=self.sender) + self.library._imap.select.assert_called_with() + self.assertTrue("imap.select error: NOK, ['1']" in context.exception) + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_raise_exception_on_search_error(self, mock_imap): + """Raise exception on imap search error.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._imap.select.return_value = ['OK', ['1']] + self.library._imap.search.return_value = ['NOK', ['']] + with self.assertRaises(Exception) as context: + self.library.wait_for_email(sender=self.sender) + self.library._imap.select.assert_called_with() + self.library._imap.search.assert_called_with(None, 'FROM', self.sender) + self.assertTrue("imap.search error: NOK, [''], criteria=['FROM', '%s']" % self.sender in context.exception) + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_delete_all_emails(self, mock_imap): + """Delete all emails.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._mails = ['0'] + self.library.delete_all_emails() + self.library._imap.store.assert_called_with('0', '+FLAGS', r'\DELETED') + self.library._imap.expunge.assert_called_with() + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_delete_email(self, mock_imap): + """Delete specific email.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library.delete_email('0') + self.library._imap.store.assert_called_with('0', '+FLAGS', r'\DELETED') + self.library._imap.expunge.assert_called_with() + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_mark_all_emails_as_read(self, mock_imap): + """Mark all emails as read.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._mails = ['0'] + self.library.mark_all_emails_as_read() + self.library._imap.store.assert_called_with('0', '+FLAGS', r'\SEEN') + + # DEPRECATED + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_mark_all_emails_as_read_from_deprecated_keyword(self, mock_imap): + """Mark all emails as read using deprecated keyword.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library._mails = ['0'] + self.library.mark_as_read() + self.library._imap.store.assert_called_with('0', '+FLAGS', r'\SEEN') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_mark_email_as_read(self, mock_imap): + """Mark specific email as read.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library.mark_email_as_read('0') + self.library._imap.store.assert_called_with('0', '+FLAGS', r'\SEEN') + + @mock.patch('ImapLibrary.IMAP4_SSL') + def test_should_close_mailbox(self, mock_imap): + """Close opened connection.""" + self.library.open_mailbox(host=self.server, user=self.username, password=self.password) + self.library.close_mailbox() + self.library._imap.close.assert_called_with()