diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9fd7fa1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig root file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[{*.robot}] +indent_size = 2 + +[*.rst] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 09bf34c..016e412 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,7 @@ -*.py[co] - -# Packages -*.egg +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] *.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg - -# Installer logs -pip-log.txt - -# Unit test / coverage reports .coverage -.tox - -#Translations -*.mo - -#Mr Developer -.mr.developer.cfg - -#vim -*.swp +dist +htmlcov diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..287920e --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,72 @@ +0.2.0 (2015.12.15) +================== + +* Transition from previous project maintainer +* Follow Python code style guide +* Initial project infrastructure + +0.1.4 (2014.04.23) +================== + +* Fix multipart-mime reading (thanks to Frank Berthold) + +0.1.3 (2014.02.28) +================== + +* Fix Gmail search contributed by https://github.com/martinhill + +0.1.2 (2014.01.16) +================== + +* Throw exception when IMAP server responds with error + +0.1.1 (2013.12.20) +================== + +* Add multipart email capabilities + +0.1.0 (2012.12.21) +================== + +* Add status filter to ``wait_for_mail`` keyword +* Fix opened page encoding to ``open_link_from_mail`` keyword + +0.0.8 (2012.11.28) +================== + +* Get email body - another attempt + +0.0.7 (2012.11.27) +================== + +* Get email body + +0.0.6 (2012.11.27) +================== + +* Mark emails as read + +0.0.5 (2012.09.25) +================== + +* Add build environment and unit test + +0.0.4 (2012.09.13) +================== + +* Add get links from email keyword + +0.0.3 (2012.08.20) +================== + +* Documents and Apache license + +0.0.2 (2012.08.20) +================== + +* from and to email are not required + +0.0.1 (2012.08.20) +================== + +* Initial version diff --git a/CHANGES.txt b/CHANGES.txt deleted file mode 100644 index d67478b..0000000 --- a/CHANGES.txt +++ /dev/null @@ -1,72 +0,0 @@ -====================================== -Changes for robotframework-imaplibrary -====================================== - -2014/04/23 0.1.4 -================ - - - Fixed multipart-mime reading (thanks to Frank Berthold) - -2014/02/28 0.1.3 -================ - - - fixed searching with gmail - contributed by https://github.com/martinhill - -2014/01/16 0.1.2 -================ - - - throw exception when IMAP server responds with error - -2013/12/20 0.1.1 -================= - - - added multipart email capabilities - -2012/12/21 0.1.0 -================ - - - added filter for status to 'wait_for_mail' - - - bugfix: use encoding from opened page while 'open_link_from_mail' - -2012/11/28 0.0.8 -================ - - - get email body - - -2012/11/27 0.0.7 -================ - - - get email body - -2012/11/27 0.0.6 -================ - - - mark mails as read - -2012/09/25 0.0.5 -================ - - - added buildout environment and unit test - -2012/09/13 0.0.4 -================ - - - added get links from email keyword - -2012/08/20 0.0.3 -================ - - - docs and apache license - -2012/08/20 0.0.2 -================ - - - from and to email not required - -2012/08/20 0.0.1 -================ - - - initial version diff --git a/COPYRIGHT.rst b/COPYRIGHT.rst new file mode 100644 index 0000000..a780185 --- /dev/null +++ b/COPYRIGHT.rst @@ -0,0 +1 @@ +# Copyright 2015 Richard Huang diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..37ec93a --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index 6708f9f..0000000 --- a/MANIFEST +++ /dev/null @@ -1,6 +0,0 @@ -# file GENERATED by distutils, do NOT edit -README.rst -setup.py -src/ImapLibrary/__init__.py -src/ImapLibrary/tests.py -src/ImapLibrary/version.py diff --git a/MANIFEST.in b/MANIFEST.in index 9561fb1..af87ba1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,3 @@ -include README.rst +include *.rst LICENSE +recursive-include doc *.html +recursive-include src *.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dc7f11a --- /dev/null +++ b/Makefile @@ -0,0 +1,66 @@ +# Copyright 2015 Richard Huang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LIBRARY_NAME = ImapLibrary + +lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1)))))))))))))))))))))))))) + +.PHONY: help test + +help: + @echo targets: clean, clean_dist, version, install_devel_deps, lint, test, doc, github_doc, testpypi, pypi + +clean: + python setup.py clean --all + rm -rf .coverage htmlcov src/*.egg-info + find . -iname "*.pyc" -delete + find . -iname "__pycache__" | xargs rm -rf {} \; + +clean_dist: + rm -rf dist + +version: + python -m robot.libdoc src/$(LIBRARY_NAME) version + +install_devel_deps: + pip install -e . + pip install coverage mock + +lint:clean + flake8 --max-complexity 10 + pylint --rcfile=setup.cfg src/$(LIBRARY_NAME)/*.py + +test:clean + PYTHONPATH=./src: coverage run --source=src -m unittest discover test/utest + coverage report + +doc:clean + python -m robot.libdoc src/$(LIBRARY_NAME) doc/$(LIBRARY_NAME).html + python -m analytics doc/$(LIBRARY_NAME).html + +github_doc:clean + git checkout gh-pages + git merge master + git push origin gh-pages + git checkout master + +testpypi:clean_dist doc + python setup.py register -r test + python setup.py sdist upload -r test --sign + @echo https://testpypi.python.org/pypi/robotframework-$(call lc,$(LIBRARY_NAME))/ + +pypi:clean_dist doc + python setup.py register -r pypi + python setup.py sdist upload -r pypi --sign + @echo https://pypi.python.org/pypi/robotframework-$(call lc,$(LIBRARY_NAME))/ diff --git a/README b/README new file mode 120000 index 0000000..92cacd2 --- /dev/null +++ b/README @@ -0,0 +1 @@ +README.rst \ No newline at end of file diff --git a/README.rst b/README.rst index e4f10da..fe89e1b 100644 --- a/README.rst +++ b/README.rst @@ -1,161 +1,247 @@ -========================== -robotframework-imaplibrary -========================== +IMAP email testing library for Robot Framework +============================================== + +|Docs| |Version| |Status| |Python| |Download| |License| + +Introduction +------------ + +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`_. + +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 | ++-----------------------------------------------------------------------------------------------+ + +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 | ++-----------------------------------------------------------------------------------------------+ -**robotframework-imaplibrary** is a `Robot Framework -`_ test library to test -mail validation tasks. +Installation +------------ +Using ``pip`` +''''''''''''' -Deprecation Warning -+++++++++++++++++++ +The recommended installation method is using pip_: -Lovely Systems does not not support this package anymore and -do not have any follow up package in the same area. If anyone is -interested to continue our efforts and would like to -manage the contributors in this open source project, -feel free to fork the package and give me a hint, so I can -create a link to your fork! +.. code:: bash -best regards, Manfred (Github: schwendinger, schwendinger at lovelysystems.com) + pip install robotframework-imaplibrary -Installation -++++++++++++ +The main benefit of using ``pip`` is that it automatically installs all +dependencies needed by the library. Other nice features are easy upgrading +and support for un-installation: -To install, just fetch the latest version from PyPI:. +.. code:: bash pip install --upgrade robotframework-imaplibrary + pip uninstall robotframework-imaplibrary + +Notice that using ``--upgrade`` above updates both the library and all +its dependencies to the latest version. If you want, you can also install +a specific version: + +.. code:: bash + + pip install robotframework-imaplibrary==x.x.x + +Proxy configuration +''''''''''''''''''' + +If you are behind a proxy, you can use ``--proxy`` command line option +or set ``http_proxy`` and/or ``https_proxy`` environment variables to +configure ``pip`` to use it. If you are behind an authenticating NTLM proxy, +you may want to consider installing CNTML_ to handle communicating with it. + +For more information about ``--proxy`` option and using pip with proxies +in general see: + +- http://pip-installer.org/en/latest/usage.html +- http://stackoverflow.com/questions/9698557/how-to-use-pip-on-windows-behind-an-authenticating-proxy +- http://stackoverflow.com/questions/14149422/using-pip-behind-a-proxy + +Manual installation +''''''''''''''''''' + +If you do not have network connection or cannot make proxy to work, you need +to resort to manual installation. This requires installing both the library +and its dependencies yourself. + +- Make sure you have `Robot Framework installed`_. + +- Download source distributions (``*.tar.gz``) for the library: + + - https://pypi.python.org/pypi/robotframework-imaplibrary + +- Download PGP signatures (``*.tar.gz.asc``) for signed packages. + +- Find each public key used to sign the package: + +.. code:: bash + + gpg --keyserver pgp.mit.edu --search-keys D1406DE7 + +- Select the number from the list to import the public key + +- Verify the package against its PGP signature: + +.. code:: bash + + gpg --verify robotframework-imaplibrary-x.x.x.tar.gz.asc robotframework-imaplibrary-x.x.x.tar.gz + +- Extract each source distribution to a temporary location. + +- Go to each created directory from the command line and install each project using: + +.. code:: bash + + python setup.py install + +If you are on Windows, and there are Windows installers available for +certain projects, you can use them instead of source distributions. +Just download 32bit or 64bit installer depending on your system, +double-click it, and follow the instructions. + +Directory Layout +---------------- + +doc/ + `Keyword documentation`_ + +src/ + Python source code + +test/ + Test files + + utest/ + Python unit test Usage -+++++ +----- -Setup in the robotframework Settings section: +To write tests with Robot Framework and ImapLibrary, +ImapLibrary must be imported into your Robot test suite. -============ ================ - Setting Value -============ ================ -Library ImapLibrary -============ ================ ++-----------------------+ +| *** Settings *** | ++---------+-------------+ +| Library | ImapLibrary | ++---------+-------------+ -\ - -These keyword actions are available:: - - Open Mailbox: - Open the mailbox on a mail server with a valid authentication: - Arguments: - - server: the server name (e.g. imap.googlemail.com) - - user: the user name (e.g. me@googlemail.com) - - password: the user's password - - Wait for Mail: - Wait for an incoming mail. Check the mailbox every 10 seconds - for incoming mails until a matching email is received or the - timeout is exceeded. Returns the mail number of the latest matching - email. - Arguments: - - fromEmail: the email address of the sender (not required) - - toEmail: the email address of the receiver (not required) - - status: the status of the email (not required) - - timeout: the timeout how long the mailbox shall check emails - in seconds (defaults to 60 seconds) - - Get Links From Email: - Finds all links in an email body and returns them - - Arguments: - - mailNumber: is the index number of the mail to open - - Get Matches From Email: - Finds all occurrences of a regular expression - - Arguments: - - mailNumber: is the index number of the mail to open - - regexp: a regular expression to find - - Open Link from Mail: - Find a link in an email body and open the link. Returns the links' html. - Arguments: - mailNumber: the number of the email to check for a link - linkNumber: the index of the link to open - (defaults to 0, which is the first link) - - Get Email body: - Returns an email body - Arguments: - mailNumber: the number of the email to check for a link - - Walk Multipart Email - 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. - - Arguments: - mailNumber: the index number of the mail to open - - Get Multipart Content Type - Return the content-type for the current part of a multipart email - - Get Multipart Payload - Return the payload for the current part of a multipart email - - Arguments: - decode: an optional flag that indicates whether to decoding - - Get Multipart Field Names - Return the list of header field names for the current multipart email - - Get Multipart Field - Returns the content of a header field - - Arguments: - field: a string such as 'From', 'To', 'Subject', 'Date', etc. - - Mark as read: - Mark all received mails as read - - Close Mailbox: - Close the mailbox after finishing all mail activities of a user. - -For more informaiton on `status` see: `Mailbox Status `_. - -Here is an example of how to use the library: - -============== ========================== =================================== ================================== ============= ============ - Action Argument Argument Argument Argument Argument -============== ========================== =================================== ================================== ============= ============ -Open Mailbox server=imap.googlemail.com user=mymail@googlemail.com password=mysecretpassword -${LATEST}= Wait for Mail fromEmail=noreply@register.com toEmail=mymailalias@googlemail.com status=UNSEEN timeout=150 -${HTML}= Open Link from Mail ${LATEST} -Should Contain ${HTML} Your email address has been updated -Close Mailbox -============== ========================== =================================== ================================== ============= ============ - -Here is an example of how to work with multipart emails, ignoring all non content-type='test/html' parts: - -============== ========================== =================================== =================================== ============ - Action Argument Argument Argument Argument -============== ========================== =================================== =================================== ============ -Open Mailbox server=imap.googlemail.com user=mymail@googlemail.com password=mysecretpassword -${LATEST}= Wait for Mail fromEmail=noreply@register.com toEmail=mymailalias@googlemail.com timeout=150 -${parts}= Walk Multipart Email ${LATEST} -@{fields}= Get Multipart Field Names -${from}= Get Multipart Field From -${to}= Get Multipart Field To -${subject}= Get Multipart Field Subject -: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} Update your email address -\ ${HTML}= Open Link from Mail ${LATEST} -\ Should Contain ${HTML} Your email address has been updated -Close Mailbox -============== ========================== =================================== =================================== ============ +See `Robot Framework User Guide`_ for more information. -License -+++++++ +More information about Robot Framework standard libraries and built-in tools +can be found in the `Robot Framework Documentation`_. + +Building Keyword Documentation +------------------------------ + +The `Keyword Documentation`_ can be found online, if you need to generate the keyword documentation, run: + +.. code:: bash -The robotframework-imaplibrary is licensed under the `Apache 2.0 License -`_. + make doc + +Run Unit Tests, and Test Coverage Report +---------------------------------------- + +Test the testing library, talking about dogfooding, let's run: + +.. code:: bash + + make test + +Contributing +------------ + +If you would like to contribute code to Imap Library project you can do so through GitHub by forking the repository and sending a pull request. + +When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Please also include appropriate test cases. + +Before your code can be accepted into the project you must also sign the `Imap Library CLA`_ (Individual Contributor License Agreement). + +That's it! Thank you for your contribution! + +License +------- + +Copyright (c) 2015 Richard Huang. + +This library is free software, licensed under: `Apache License, Version 2.0`_. + +Documentation and other similar content are provided under `Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License`_. + +.. _Apache License, Version 2.0: https://goo.gl/qpvnnB +.. _CNTML: http://goo.gl/ukiwSO +.. _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 +.. |Docs| image:: https://img.shields.io/badge/docs-latest-brightgreen.svg + :target: https://goo.gl/ntRuxC + :alt: Keyword Documentation +.. |Version| image:: https://img.shields.io/pypi/v/robotframework-imaplibrary.svg + :target: https://goo.gl/q66LcA + :alt: Package Version +.. |Status| image:: https://img.shields.io/pypi/status/robotframework-imaplibrary.svg + :target: https://goo.gl/q66LcA + :alt: Development Status +.. |Python| image:: https://img.shields.io/pypi/pyversions/robotframework-imaplibrary.svg + :target: https://goo.gl/sXzgao + :alt: Python Version +.. |Download| image:: https://img.shields.io/pypi/dm/robotframework-imaplibrary.svg + :target: https://goo.gl/q66LcA + :alt: Monthly Download +.. |License| image:: https://img.shields.io/pypi/l/robotframework-imaplibrary.svg + :target: https://goo.gl/qpvnnB + :alt: License diff --git a/analytics.py b/analytics.py new file mode 100644 index 0000000..f48c23b --- /dev/null +++ b/analytics.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Richard Huang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +IMAP Library - a IMAP email testing library. +""" + +from __future__ import print_function +from os.path import split +from re import sub +import sys + + +def main(argv): + """Adds analytics code into auto generated documentation.""" + try: + path = argv[0] + except IndexError: + print("analytics.py ") + sys.exit(1) + + with open(path) as reader: + content = reader.read() + + analytics = """""" % (split(path)[1]) + + content = sub(r"", analytics + "\n", content) + + with open(path, "w") as writer: + writer.write(content) + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100644 index 5f2cb08..0000000 --- a/bootstrap.py +++ /dev/null @@ -1,260 +0,0 @@ -############################################################################## -# -# Copyright (c) 2006 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Bootstrap a buildout-based project - -Simply run this script in a directory containing a buildout.cfg. -The script accepts buildout command-line options, so you can -use the -c option to specify an alternate configuration file. -""" - -import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess -from optparse import OptionParser - -if sys.platform == 'win32': - def quote(c): - if ' ' in c: - return '"%s"' % c # work around spawn lamosity on windows - else: - return c -else: - quote = str - -# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. -stdout, stderr = subprocess.Popen( - [sys.executable, '-Sc', - 'try:\n' - ' import ConfigParser\n' - 'except ImportError:\n' - ' print 1\n' - 'else:\n' - ' print 0\n'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() -has_broken_dash_S = bool(int(stdout.strip())) - -# In order to be more robust in the face of system Pythons, we want to -# run without site-packages loaded. This is somewhat tricky, in -# particular because Python 2.6's distutils imports site, so starting -# with the -S flag is not sufficient. However, we'll start with that: -if not has_broken_dash_S and 'site' in sys.modules: - # We will restart with python -S. - args = sys.argv[:] - args[0:0] = [sys.executable, '-S'] - args = map(quote, args) - os.execv(sys.executable, args) -# Now we are running with -S. We'll get the clean sys.path, import site -# because distutils will do it later, and then reset the path and clean -# out any namespace packages from site-packages that might have been -# loaded by .pth files. -clean_path = sys.path[:] -import site -sys.path[:] = clean_path -for k, v in sys.modules.items(): - if k in ('setuptools', 'pkg_resources') or ( - hasattr(v, '__path__') and - len(v.__path__)==1 and - not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))): - # This is a namespace package. Remove it. - sys.modules.pop(k) - -is_jython = sys.platform.startswith('java') - -setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' -distribute_source = 'http://python-distribute.org/distribute_setup.py' - -# parsing arguments -def normalize_to_url(option, opt_str, value, parser): - if value: - if '://' not in value: # It doesn't smell like a URL. - value = 'file://%s' % ( - urllib.pathname2url( - os.path.abspath(os.path.expanduser(value))),) - if opt_str == '--download-base' and not value.endswith('/'): - # Download base needs a trailing slash to make the world happy. - value += '/' - else: - value = None - name = opt_str[2:].replace('-', '_') - setattr(parser.values, name, value) - -usage = '''\ -[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] - -Bootstraps a buildout-based project. - -Simply run this script in a directory containing a buildout.cfg, using the -Python that you want bin/buildout to use. - -Note that by using --setup-source and --download-base to point to -local resources, you can keep this script from going over the network. -''' - -parser = OptionParser(usage=usage) -parser.add_option("-v", "--version", dest="version", - help="use a specific zc.buildout version") -parser.add_option("-d", "--distribute", - action="store_true", dest="use_distribute", default=False, - help="Use Distribute rather than Setuptools.") -parser.add_option("--setup-source", action="callback", dest="setup_source", - callback=normalize_to_url, nargs=1, type="string", - help=("Specify a URL or file location for the setup file. " - "If you use Setuptools, this will default to " + - setuptools_source + "; if you use Distribute, this " - "will default to " + distribute_source +".")) -parser.add_option("--download-base", action="callback", dest="download_base", - callback=normalize_to_url, nargs=1, type="string", - help=("Specify a URL or directory for downloading " - "zc.buildout and either Setuptools or Distribute. " - "Defaults to PyPI.")) -parser.add_option("--eggs", - help=("Specify a directory for storing eggs. Defaults to " - "a temporary directory that is deleted when the " - "bootstrap script completes.")) -parser.add_option("-t", "--accept-buildout-test-releases", - dest='accept_buildout_test_releases', - action="store_true", default=False, - help=("Normally, if you do not specify a --version, the " - "bootstrap script and buildout gets the newest " - "*final* versions of zc.buildout and its recipes and " - "extensions for you. If you use this flag, " - "bootstrap and buildout will get the newest releases " - "even if they are alphas or betas.")) -parser.add_option("-c", None, action="store", dest="config_file", - help=("Specify the path to the buildout configuration " - "file to be used.")) - -options, args = parser.parse_args() - -# if -c was provided, we push it back into args for buildout's main function -if options.config_file is not None: - args += ['-c', options.config_file] - -if options.eggs: - eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) -else: - eggs_dir = tempfile.mkdtemp() - -if options.setup_source is None: - if options.use_distribute: - options.setup_source = distribute_source - else: - options.setup_source = setuptools_source - -if options.accept_buildout_test_releases: - args.append('buildout:accept-buildout-test-releases=true') -args.append('bootstrap') - -try: - import pkg_resources - import setuptools # A flag. Sometimes pkg_resources is installed alone. - if not hasattr(pkg_resources, '_distribute'): - raise ImportError -except ImportError: - ez_code = urllib2.urlopen( - options.setup_source).read().replace('\r\n', '\n') - ez = {} - exec ez_code in ez - setup_args = dict(to_dir=eggs_dir, download_delay=0) - if options.download_base: - setup_args['download_base'] = options.download_base - if options.use_distribute: - setup_args['no_fake'] = True - ez['use_setuptools'](**setup_args) - if 'pkg_resources' in sys.modules: - reload(sys.modules['pkg_resources']) - import pkg_resources - # This does not (always?) update the default working set. We will - # do it. - for path in sys.path: - if path not in pkg_resources.working_set.entries: - pkg_resources.working_set.add_entry(path) - -cmd = [quote(sys.executable), - '-c', - quote('from setuptools.command.easy_install import main; main()'), - '-mqNxd', - quote(eggs_dir)] - -if not has_broken_dash_S: - cmd.insert(1, '-S') - -find_links = options.download_base -if not find_links: - find_links = os.environ.get('bootstrap-testing-find-links') -if find_links: - cmd.extend(['-f', quote(find_links)]) - -if options.use_distribute: - setup_requirement = 'distribute' -else: - setup_requirement = 'setuptools' -ws = pkg_resources.working_set -setup_requirement_path = ws.find( - pkg_resources.Requirement.parse(setup_requirement)).location -env = dict( - os.environ, - PYTHONPATH=setup_requirement_path) - -requirement = 'zc.buildout' -version = options.version -if version is None and not options.accept_buildout_test_releases: - # Figure out the most recent final version of zc.buildout. - import setuptools.package_index - _final_parts = '*final-', '*final' - def _final_version(parsed_version): - for part in parsed_version: - if (part[:1] == '*') and (part not in _final_parts): - return False - return True - index = setuptools.package_index.PackageIndex( - search_path=[setup_requirement_path]) - if find_links: - index.add_find_links((find_links,)) - req = pkg_resources.Requirement.parse(requirement) - if index.obtain(req) is not None: - best = [] - bestv = None - for dist in index[req.project_name]: - distv = dist.parsed_version - if _final_version(distv): - if bestv is None or distv > bestv: - best = [dist] - bestv = distv - elif distv == bestv: - best.append(dist) - if best: - best.sort() - version = best[-1].version -if version: - requirement = '=='.join((requirement, version)) -cmd.append(requirement) - -if is_jython: - import subprocess - exitcode = subprocess.Popen(cmd, env=env).wait() -else: # Windows prefers this, apparently; otherwise we would prefer subprocess - exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) -if exitcode != 0: - sys.stdout.flush() - sys.stderr.flush() - print ("An error occurred when trying to install zc.buildout. " - "Look above this message for any errors that " - "were output by easy_install.") - sys.exit(exitcode) - -ws.add_entry(eggs_dir) -ws.require(requirement) -import zc.buildout.buildout -zc.buildout.buildout.main(args) -if not options.eggs: # clean up temporary egg directory - shutil.rmtree(eggs_dir) diff --git a/buildout.cfg b/buildout.cfg deleted file mode 100644 index ae8084e..0000000 --- a/buildout.cfg +++ /dev/null @@ -1,12 +0,0 @@ -[buildout] -develop = . -parts = test -versions = versions - -[versions] -zope.testing = 4.0.0 - -[test] -recipe = corejet.testrunner -defaults = ['--auto-color', '--tests-pattern', '^f?tests$'] -eggs = robotframework-imaplibrary [test] diff --git a/doc/ImapLibrary.html b/doc/ImapLibrary.html new file mode 100644 index 0000000..eff6bb4 --- /dev/null +++ b/doc/ImapLibrary.html @@ -0,0 +1,840 @@ + + + + + + + + + + + + + + + + + + + + + + +
+

Opening library documentation failed

+
    +
  • Verify that you have JavaScript enabled in your browser.
  • +
  • Make sure you are using a modern enough browser. Firefox 3.5, IE 8, or equivalent is required, newer browsers are recommended.
  • +
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • +
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..97fcd29 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,15 @@ +[BASIC] +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,40}$ + +[flake8] +ignore=E501 + +[MESSAGES CONTROL] +disable=locally-disabled + +[sdist] +formats=gztar,zip + +[wheel] +universal=1 diff --git a/setup.py b/setup.py index 78c8b03..557df88 100644 --- a/setup.py +++ b/setup.py @@ -1,33 +1,56 @@ -#!/usr/bin/env python - -from os.path import join, dirname - -execfile(join(dirname(__file__), 'src', 'ImapLibrary', 'version.py')) - -from distutils.core import setup - -CLASSIFIERS = """ -Programming Language :: Python -Topic :: Software Development :: Testing -"""[1:-1] - -long_description=open(join(dirname(__file__), 'README.rst',)).read() - -setup( - name = 'robotframework-imaplibrary', - version = VERSION, - description = 'Robot Framework IMAP Mail Check Library', - long_description = long_description, - author = 'Lovely Systems GmbH', - author_email = 'office@lovelysystems.com', - url = 'https://github.com/lovelysystems/robotframework-imaplibrary', - license = 'Apache License 2.0', - keywords = 'robotframework testing testautomation imap mail', - platforms = 'any', - zip_safe = False, - classifiers = CLASSIFIERS.splitlines(), - package_dir = {'' : 'src'}, - install_requires = ['robotframework'], - extras_require = dict(test=['zope.testing']), - packages = ['ImapLibrary'], -) +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Richard Huang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +IMAP Library - a IMAP email testing library. +""" + +# To use a consistent encoding +import codecs +from os.path import abspath, dirname, join +# Always prefer setuptools over distutils +from setuptools import setup, find_packages + +LIBRARY_NAME = 'ImapLibrary' +CWD = abspath(dirname(__file__)) +execfile(join(CWD, 'src', LIBRARY_NAME, 'version.py')) + +with codecs.open(join(CWD, 'README.rst'), encoding='utf-8') as reader: + LONG_DESCRIPTION = reader.read() + +setup( + name='robotframework-%s' % LIBRARY_NAME.lower(), + version=VERSION, # pylint: disable=undefined-variable # noqa + description='A IMAP email testing library for Robot Framework', + long_description=LONG_DESCRIPTION, + url='https://github.com/rickypc/robotframework-%s' % LIBRARY_NAME.lower(), + author='Richard Huang', + author_email='rickypc@users.noreply.github.com', + license='Apache License, Version 2.0', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Testing', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 2.7', + ], + keywords='robot framework testing automation imap email mail softwaretesting', + platforms='any', + packages=find_packages('src'), + package_dir={'': 'src'}, + install_requires=['robotframework >= 2.6.0'] +) diff --git a/src/ImapLibrary/__init__.py b/src/ImapLibrary/__init__.py index 68f4b5c..c42e33a 100644 --- a/src/ImapLibrary/__init__.py +++ b/src/ImapLibrary/__init__.py @@ -1,82 +1,144 @@ -import re -import os +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Richard Huang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +IMAP Library - a IMAP email testing library. +""" + +from ImapLibrary.version import get_version +from re import findall import imaplib import time import urllib2 import email -THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -execfile(os.path.join(THIS_DIR, 'version.py')) - -__version__ = VERSION +__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* + + 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]. + + 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` | | | | + + 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` | | | | + """ + # pylint: disable=line-too-long - ROBOT_LIBRARY_VERSION = VERSION ROBOT_LIBRARY_SCOPE = 'GLOBAL' + ROBOT_LIBRARY_VERSION = __version__ - port = 993 + def __init__(self): + """ImapLibrary can be imported without argument. - def open_mailbox(self, server, user, password): + Examples: + | = Keyword Definition = | = Description = | + | Library `|` ImapLibrary | Initiate Imap library | """ - Open the mailbox on a mail server with a valid - authentication. + self._email_index = None + self._imap = None + self._mails = [] + 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. """ - self.imap = imaplib.IMAP4_SSL(server, self.port) - self.imap.login(user, password) - self.imap.select() - self._init_walking_multipart() + self._imap = imaplib.IMAP4_SSL(server, self._port) + self._imap.login(user, password) + self._imap.select() + self._init_multipart_walk() - def wait_for_mail(self, fromEmail=None, toEmail=None, status=None, - timeout=60): + def wait_for_mail(self, from_email=None, to_email=None, status=None, timeout=60): """ 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. - `timeout` sets the maximum waiting time until an error - is raised. + ``status`` is a mailbox status filter. + Please see [https://goo.gl/3KKHoY|Mailbox Status] for more information. + + `timeout` sets the maximum waiting time until an error is raised. """ - endTime = time.time() + int(timeout) - while (time.time() < endTime): - self.mails = self._check_emails(fromEmail, toEmail, status) - if len(self.mails) > 0: - return self.mails[-1] - if time.time() < endTime: + 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") - def get_links_from_email(self, mailNumber): + def get_links_from_email(self, email_index): ''' Finds all links in an email body and returns them - `mailNumber` is the index number of the mail to open + `email_index` is the index number of the mail to open ''' - body = self.get_email_body(mailNumber) - return re.findall(r'href=[\'"]?([^\'" >]+)', body) + body = self.get_email_body(email_index) + return findall(r'href=[\'"]?([^\'" >]+)', body) - def get_matches_from_email(self, mailNumber, regexp): + def get_matches_from_email(self, email_index, regexp): """ Finds all occurrences of a regular expression """ - body = self.get_email_body(mailNumber) - return re.findall(regexp, body) + body = self.get_email_body(email_index) + return findall(regexp, body) - def open_link_from_mail(self, mailNumber, linkNumber=0): + def open_link_from_mail(self, email_index, link_index=0): """ Find a link in an email body and open the link. Returns the link's html. - `mailNumber` is the index number of the mail to open - `linkNumber` declares which link shall be opened (link + `email_index` is the index number of the mail to open + `link_index` declares which link shall be opened (link index in body text) """ - urls = self.get_links_from_email(mailNumber) + urls = self.get_links_from_email(email_index) - if len(urls) > linkNumber: - resp = urllib2.urlopen(urls[linkNumber]) + if len(urls) > link_index: + resp = urllib2.urlopen(urls[link_index]) content_type = resp.headers.getheader('content-type') if content_type: enc = content_type.split('charset=')[-1] @@ -84,78 +146,62 @@ def open_link_from_mail(self, mailNumber, linkNumber=0): else: return resp.read() else: - raise AssertionError("Link number %i not found!" % linkNumber) + raise AssertionError("Link number %i not found!" % link_index) - def delete_email(self, mailNumber): + def delete_email(self, email_index): """ Delete the selected email. """ - body = self.imap.store(mailNumber, '+FLAGS', '\\Deleted') - self.imap.expunge() + self._imap.store(email_index, '+FLAGS', '\\Deleted') + self._imap.expunge() def close_mailbox(self): """ Close the mailbox after finishing all mail activities of a user. """ - self.imap.close() + self._imap.close() def mark_as_read(self): """ Mark all received mails as read """ - for mail in self.mails: - self.imap.store(mail, '+FLAGS', '\SEEN') + for mail in self._mails: + self._imap.store(mail, '+FLAGS', r'\SEEN') - def get_email_body(self, mailNumber): + def get_email_body(self, email_index): """ Returns an email body - `mailNumber` is the index number of the mail to open + `email_index` is the index number of the mail to open """ - if self._is_walking_multipart(mailNumber): + if self._is_walking_multipart(email_index): body = self.get_multipart_payload(decode=True) else: - body = self.imap.fetch(mailNumber, '(BODY[TEXT])')[1][0][1].decode('quoted-printable') + body = self._imap.fetch(email_index, '(BODY[TEXT])')[1][0][1].decode('quoted-printable') return body - def walk_multipart_email(self, mailNumber): + 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. - `mailNumber` is the index number of the mail to open + `email_index` is the index number of the mail to open """ - if not self._is_walking_multipart(mailNumber): - data = self.imap.fetch(mailNumber, '(RFC822)')[1][0][1] + 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_walking_multipart(mailNumber, msg) + self._start_multipart_walk(email_index, msg) try: self._part = next(self._mp_iter) except StopIteration: - self._init_walking_multipart() + self._init_multipart_walk() return False # return number of parts return len(self._mp_msg.get_payload()) - def _is_walking_multipart(self, mailNumber): - """ - Check if walking a multipart email is in progress - """ - return self._mp_msg is not None and self._mailNumber == mailNumber - - def _start_walking_multipart(self, mailNumber, msg): - self._mailNumber = mailNumber - self._mp_msg = msg - self._mp_iter = msg.walk() - - def _init_walking_multipart(self): - self._mp_msg = None - self._part = None - self._mailNumber = None - def get_multipart_content_type(self): """ Return the content-type for the current part of a multipart email @@ -168,45 +214,64 @@ def get_multipart_payload(self, decode=False): decode is an optional flag that indicates whether to decoding """ - s = self._part.get_payload(decode=decode) + payload = self._part.get_payload(decode=decode) charset = self._part.get_content_charset() if charset is not None: - return s.decode(charset) - return s + 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 self._mp_msg.keys() + return list(self._mp_msg.keys()) def get_multipart_field(self, field): """ - Returns the content of a header 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 _criteria(self, fromEmail, toEmail, status): + def _check_emails(self, from_email, to_email, status): + """Returns filtered email.""" + crit = self._criteria(from_email, to_email, status) + # 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) + if typ != 'OK': + raise Exception('imap.search error: %s, %s, criteria=%s' % (typ, msgnums, crit)) + return msgnums[0].split() + + @staticmethod + def _criteria(from_email, to_email, status): + """Returns email criteria.""" crit = [] - if fromEmail: - crit += ['FROM', fromEmail] - if toEmail: - crit += ['TO', toEmail] + if from_email: + crit += ['FROM', from_email] + if to_email: + crit += ['TO', to_email] if status: crit += [status] if not crit: crit = ['UNSEEN'] return crit - def _check_emails(self, fromEmail, toEmail, status): - crit = self._criteria(fromEmail, toEmail, status) - # 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) - if typ != 'OK': - raise Exception('imap.search error: ' + typ + ', ' + str(msgnums) + ' criterion=' + str(crit)) - return msgnums[0].split() + def _init_multipart_walk(self): + """Initialize multipart email walk.""" + self._email_index = None + self._mp_msg = None + self._part = None + + def _is_walking_multipart(self, email_index): + """Returns boolean value whether the multipart email walk is in-progress or not.""" + return self._mp_msg is not None and self._email_index == email_index + + def _start_multipart_walk(self, email_index, msg): + """Start multipart email walk.""" + self._email_index = email_index + self._mp_msg = msg + self._mp_iter = msg.walk() diff --git a/src/ImapLibrary/mail.txt b/src/ImapLibrary/mail.txt deleted file mode 100644 index 09d16b0..0000000 --- a/src/ImapLibrary/mail.txt +++ /dev/null @@ -1,117 +0,0 @@ -To get a valid mail account we have to set these environment variables -to get the test running. We recommend to use a google account as it supports -the plus notation (someone+111@gmail.com), which is needed for this test:: - - export IMAPLIBRARY_TEST_SERVER=imap.googlemail.com - export IMAPLIBRARY_TEST_USER=yourgoogleaccount@gmail.com - export IMAPLIBRARY_TEST_PASSWORD=yourgooglepassword - -The test gets the email account data from the environment variables:: - - >>> import os - >>> server = os.environ['IMAPLIBRARY_TEST_SERVER'] - >>> user = os.environ['IMAPLIBRARY_TEST_USER'] - >>> password = os.environ['IMAPLIBRARY_TEST_PASSWORD'] - -Prepare to plus notation for the receiver email address:: - - >>> import time - >>> timestamp = int(time.time() * 1000) - >>> split = user.split("@") - >>> fromEmail = user - >>> toEmail = '%s+%s@%s' % (split[0], timestamp, split[1]) - -Let's prepare an email containing some links. The sender and the receiver are -the same:: - - >>> import smtplib - >>> message = """From: From ImapLibraryTest <%s> - ... To: To Tester <%s> - ... Subject: E-Mail-Test %s - ... - ... This is a test email message with two links. - ... Link 1 - ... Link 2 - ... """ % (fromEmail, toEmail, timestamp) - -Open the mailbox:: - - >>> from ImapLibrary import ImapLibrary - >>> imap = ImapLibrary() - >>> imap.open_mailbox(server, user, password) - -Now let's send the email with the same account:: - - >>> smtpObj = smtplib.SMTP('smtp.gmail.com') - >>> ignore = smtpObj.ehlo() - >>> ignore = smtpObj.starttls() - >>> ignore = smtpObj.ehlo() - >>> ignore = smtpObj.login(user, password) - >>> smtpObj.sendmail(fromEmail, [toEmail], message) - {} - -With the wait_for_mail method we should get an email sent to the specific -timestamped address. The timeout specifies the time we wait for the email -to be delivered. The method returns the mail number in the mail box:: - - >>> mailNr = imap.wait_for_mail(toEmail=toEmail, timeout=60) - >>> mailNr is not None - True - -We can get the links of that mail number:: - - >>> imap.get_links_from_email(mailNr) - ['http://www.google.de', 'http://www.lovelysystems.com'] - -And we can get the content of the link:: - - >>> imap.open_link_from_mail(mailNr, 0) - u'<...html...Google...' - -We can also wait for an email from a specific address with the fromEmail -parameter. If there is no email from that specific sender, we get an -AssertionError:: - - >>> imap.wait_for_mail(fromEmail="thisoneneversentamail@lovelysystems.com", - ... timeout=10) - Traceback (most recent call last): - ... - AssertionError: No mail received within time - -We can also wait for unread mails:: - - >>> ig = smtpObj.sendmail(fromEmail, [toEmail], message) - >>> ig = smtpObj.sendmail(fromEmail, [toEmail], message) - >>> mailNr = imap.wait_for_mail(timeout=10) - >>> mailNr is not None - True - -It is possible to filter messages for a specific status. Allowed status are: - - UNSEEN, ALL, MESSAGES, RECENT, UIDNEXT, UIDVALIDITY - -The status might be combined with other filters:: - - >>> mailNr = imap.wait_for_mail(fromEmail=fromEmail,toEmail=toEmail, - ... status='UNSEEN', timeout=10) - >>> mailNr is not None - True - -We can mark the mails as read:: - - >>> imap.mark_as_read() - >>> imap.wait_for_mail(timeout=10) - Traceback (most recent call last): - ... - AssertionError: No mail received within time - -Read mails:: - - >>> ig = smtpObj.sendmail(fromEmail, [toEmail], message) - >>> latest = imap.wait_for_mail(timeout=10) - >>> print imap.get_email_body(latest) - This is a test email ... - -Finally we can close the mail box again:: - - >>> imap.close_mailbox() diff --git a/src/ImapLibrary/tests.py b/src/ImapLibrary/tests.py deleted file mode 100644 index 0c4138e..0000000 --- a/src/ImapLibrary/tests.py +++ /dev/null @@ -1,37 +0,0 @@ -############################################################################## -# -# Copyright (c) 2006-2012 Lovely Systems AG. All Rights Reserved. -# -# This software is subject to the provisions of the Lovely Visible Source -# License, Version 1.0 (LVSL). A copy of the LVSL should accompany this -# distribution. -# -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -""" -""" -__docformat__ = 'restructuredtext' - -import unittest -from zope.testing import doctest -from zope.testing.doctestunit import DocFileSuite, DocTestSuite - -def uSuite(testfile, level=None): - suite = doctest.DocFileSuite( - testfile, - optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS) - if level is not None: - suite.level = level - return suite - - -def test_suite(): - s = unittest.TestSuite(( - uSuite('mail.txt', level=2), - )) - return s - diff --git a/src/ImapLibrary/version.py b/src/ImapLibrary/version.py index 93686cd..0cfc0ca 100644 --- a/src/ImapLibrary/version.py +++ b/src/ImapLibrary/version.py @@ -1 +1,27 @@ -VERSION = '0.1.4' +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Richard Huang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +IMAP Library - a IMAP email testing library. +""" + +VERSION = '0.2.0' + + +def get_version(): + """Returns the current version.""" + return VERSION diff --git a/test/utest/test_imaplibrary.py b/test/utest/test_imaplibrary.py new file mode 100644 index 0000000..85b7a83 --- /dev/null +++ b/test/utest/test_imaplibrary.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Richard Huang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +IMAP Library - a IMAP email testing library. +""" + +from sys import path +path.append('src') +from ImapLibrary import ImapLibrary +import mock +import unittest + + +class ImapLibraryTests(unittest.TestCase): + """Imap library test class.""" + + def setUp(self): + """Instantiate the Imap library class.""" + self.library = ImapLibrary() + self.password = 'password' + self.secure_port = 993 + self.server = 'my.imap' + self.username = 'username' + + def test_should_have_default_values(self): + """Imap library instance should have default values set.""" + self.assertIsInstance(self.library, ImapLibrary) + self.assertIsNone(self.library._email_index) + self.assertIsNone(self.library._imap) + self.assertIsInstance(self.library._mails, list) + 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) + + @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) + self.library._imap.login.assert_called_with(self.username, self.password) + self.library._imap.select.assert_called_with()