├── .editorconfig ├── .gitignore ├── CHANGELOG.rst ├── COPYRIGHT.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README ├── README.rst ├── analytics.py ├── doc └── ImapLibrary.html ├── setup.cfg ├── setup.py ├── src └── ImapLibrary │ ├── __init__.py │ └── version.py └── test └── utest └── test_imaplibrary.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig root file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.robot}] 14 | indent_size = 2 15 | 16 | [*.rst] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.egg-info 5 | .coverage 6 | dist 7 | htmlcov 8 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.3.0 (2016.11.09) 2 | ================== 3 | 4 | * Use imaplib `uid` function to avoid races with concurrent IMAP clients. 5 | Thanks to @gsikorski 6 | 7 | 0.2.5 (2016.10.31) 8 | ================== 9 | 10 | * Added custom folder support for `Open Mailbox` and `Wait For Email`. 11 | Thanks to @jhoward321 12 | 13 | 0.2.4 (2016.09.17) 14 | ================== 15 | 16 | * Fix is_secure parameter. Thanks to @davidrums 17 | 18 | 0.2.3 (2016.01.19) 19 | ================== 20 | 21 | * Multi-words email subject search bugfixes 22 | * Adjust documentation 23 | * Adjust test cases 24 | 25 | 0.2.2 (2016.01.19) 26 | ================== 27 | 28 | * Adjust documentation 29 | * Add Python 3.x support 30 | 31 | 0.2.1 (2015.12.20) 32 | ================== 33 | 34 | * Add subject and text filters 35 | * Add non-secure connection support 36 | * Adjust documentation 37 | * Add more unit test 38 | * Add backward compatible support 39 | * Add `Delete All Emails`, `Delete Email`, `Mark All Emails As Read`, 40 | and `Mark Email As Read` keywords 41 | * Add alternative keyword to deprecated keywords 42 | 43 | 0.2.0 (2015.12.15) 44 | ================== 45 | 46 | * Transition from previous project maintainer 47 | * Follow Python code style guide 48 | * Initial project infrastructure 49 | 50 | 0.1.4 (2014.04.23) 51 | ================== 52 | 53 | * Fix multipart-mime reading (thanks to Frank Berthold) 54 | 55 | 0.1.3 (2014.02.28) 56 | ================== 57 | 58 | * Fix Gmail search contributed by https://github.com/martinhill 59 | 60 | 0.1.2 (2014.01.16) 61 | ================== 62 | 63 | * Throw exception when IMAP server responds with error 64 | 65 | 0.1.1 (2013.12.20) 66 | ================== 67 | 68 | * Add multipart email capabilities 69 | 70 | 0.1.0 (2012.12.21) 71 | ================== 72 | 73 | * Add status filter to ``wait_for_mail`` keyword 74 | * Fix opened page encoding to ``open_link_from_mail`` keyword 75 | 76 | 0.0.8 (2012.11.28) 77 | ================== 78 | 79 | * Get email body - another attempt 80 | 81 | 0.0.7 (2012.11.27) 82 | ================== 83 | 84 | * Get email body 85 | 86 | 0.0.6 (2012.11.27) 87 | ================== 88 | 89 | * Mark emails as read 90 | 91 | 0.0.5 (2012.09.25) 92 | ================== 93 | 94 | * Add build environment and unit test 95 | 96 | 0.0.4 (2012.09.13) 97 | ================== 98 | 99 | * Add get links from email keyword 100 | 101 | 0.0.3 (2012.08.20) 102 | ================== 103 | 104 | * Documents and Apache license 105 | 106 | 0.0.2 (2012.08.20) 107 | ================== 108 | 109 | * from and to email are not required 110 | 111 | 0.0.1 (2012.08.20) 112 | ================== 113 | 114 | * Initial version 115 | -------------------------------------------------------------------------------- /COPYRIGHT.rst: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 Richard Huang 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE 2 | recursive-include doc *.html 3 | recursive-include src *.py 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 Richard Huang 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | LIBRARY_NAME = ImapLibrary 16 | 17 | 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)))))))))))))))))))))))))) 18 | 19 | .PHONY: help test 20 | 21 | help: 22 | @echo targets: clean, clean_dist, version, install_devel_deps, lint, test, doc, github_doc, testpypi, pypi 23 | 24 | clean: 25 | python setup.py clean --all 26 | rm -rf .coverage htmlcov src/*.egg-info 27 | find . -iname "*.pyc" -delete 28 | find . -iname "__pycache__" | xargs rm -rf {} \; 29 | 30 | clean_dist: 31 | rm -rf dist 32 | 33 | version: 34 | python -m robot.libdoc src/$(LIBRARY_NAME) version 35 | 36 | install_devel_deps: 37 | pip install -e . 38 | pip install coverage mock 39 | 40 | lint:clean 41 | flake8 --max-complexity 10 src/$(LIBRARY_NAME)/*.py 42 | pylint --rcfile=setup.cfg src/$(LIBRARY_NAME)/*.py 43 | 44 | test:clean 45 | PYTHONPATH=./src: coverage run --source=src -m unittest discover test/utest 46 | coverage report 47 | 48 | doc:clean 49 | python -m robot.libdoc src/$(LIBRARY_NAME) doc/$(LIBRARY_NAME).html 50 | python -m analytics doc/$(LIBRARY_NAME).html 51 | 52 | github_doc:clean 53 | git checkout gh-pages 54 | git merge master 55 | git push origin gh-pages 56 | git checkout master 57 | 58 | testpypi:clean_dist doc 59 | python setup.py register -r test 60 | python setup.py sdist upload -r test --sign 61 | @echo https://testpypi.python.org/pypi/robotframework-$(call lc,$(LIBRARY_NAME))/ 62 | 63 | pypi:clean_dist doc 64 | python setup.py register -r pypi 65 | python setup.py sdist upload -r pypi --sign 66 | @echo https://pypi.python.org/pypi/robotframework-$(call lc,$(LIBRARY_NAME))/ 67 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | IMAP email testing library for Robot Framework 2 | ============================================== 3 | 4 | |Docs| |Version| |Status| |Python| |Download| |License| 5 | 6 | Introduction 7 | ------------ 8 | 9 | ImapLibrary is a IMAP email testing library for `Robot Framework`_. 10 | 11 | More information about this library can be found in the `Keyword Documentation`_. 12 | 13 | Maintainership Transfer 14 | ----------------------- 15 | 16 | Please note the new authoritative git repository for `robotframework-imaplibrary`_ package is: 17 | https://github.com/rickypc/robotframework-imaplibrary 18 | 19 | `robotframework-imaplibrary`_ package ownership is transitioned to me as the new project maintainer. 20 | 21 | I will go through the pull requests from previous repository, as well as issue list. 22 | I will try to accomodate as much as I could as time permit. **There is no need to re-post.** 23 | 24 | If you are interested to contribute back to this project, please see **Contributing** section. 25 | 26 | Examples 27 | '''''''' 28 | 29 | .. code:: robotframework 30 | 31 | *** Settings *** 32 | Library ImapLibrary 33 | 34 | *** Test Cases *** 35 | Email Verification 36 | Open Mailbox host=imap.domain.com user=email@domain.com password=secret 37 | ${LATEST} = Wait For Email sender=noreply@domain.com timeout=300 38 | ${HTML} = Open Link From Email ${LATEST} 39 | Should Contain ${HTML} Your email address has been updated 40 | Close Mailbox 41 | 42 | Multipart Email Verification 43 | Open Mailbox host=imap.domain.com user=email@domain.com password=secret 44 | ${LATEST} = Wait For Email sender=noreply@domain.com timeout=300 45 | ${parts} = Walk Multipart Email ${LATEST} 46 | :FOR ${i} IN RANGE ${parts} 47 | \\ Walk Multipart Email ${LATEST} 48 | \\ ${content-type} = Get Multipart Content Type 49 | \\ Continue For Loop If '${content-type}' != 'text/html' 50 | \\ ${payload} = Get Multipart Payload decode=True 51 | \\ Should Contain ${payload} your email 52 | \\ ${HTML} = Open Link From Email ${LATEST} 53 | \\ Should Contain ${HTML} Your email 54 | Close Mailbox 55 | 56 | Installation 57 | ------------ 58 | 59 | Using ``pip`` 60 | ''''''''''''' 61 | 62 | The recommended installation method is using pip_: 63 | 64 | .. code:: console 65 | 66 | pip install robotframework-imaplibrary 67 | 68 | The main benefit of using ``pip`` is that it automatically installs all 69 | dependencies needed by the library. Other nice features are easy upgrading 70 | and support for un-installation: 71 | 72 | .. code:: console 73 | 74 | pip install --upgrade robotframework-imaplibrary 75 | pip uninstall robotframework-imaplibrary 76 | 77 | Notice that using ``--upgrade`` above updates both the library and all 78 | its dependencies to the latest version. If you want, you can also install 79 | a specific version: 80 | 81 | .. code:: console 82 | 83 | pip install robotframework-imaplibrary==x.x.x 84 | 85 | Proxy configuration 86 | ''''''''''''''''''' 87 | 88 | If you are behind a proxy, you can use ``--proxy`` command line option 89 | or set ``http_proxy`` and/or ``https_proxy`` environment variables to 90 | configure ``pip`` to use it. If you are behind an authenticating NTLM proxy, 91 | you may want to consider installing CNTML_ to handle communicating with it. 92 | 93 | For more information about ``--proxy`` option and using pip with proxies 94 | in general see: 95 | 96 | - http://pip-installer.org/en/latest/usage.html 97 | - http://stackoverflow.com/questions/9698557/how-to-use-pip-on-windows-behind-an-authenticating-proxy 98 | - http://stackoverflow.com/questions/14149422/using-pip-behind-a-proxy 99 | 100 | Manual installation 101 | ''''''''''''''''''' 102 | 103 | If you do not have network connection or cannot make proxy to work, you need 104 | to resort to manual installation. This requires installing both the library 105 | and its dependencies yourself. 106 | 107 | - Make sure you have `Robot Framework installed`_. 108 | 109 | - Download source distributions (``*.tar.gz``) for the library: 110 | 111 | - https://pypi.python.org/pypi/robotframework-imaplibrary 112 | 113 | - Download PGP signatures (``*.tar.gz.asc``) for signed packages. 114 | 115 | - Find each public key used to sign the package: 116 | 117 | .. code:: console 118 | 119 | gpg --keyserver pgp.mit.edu --search-keys D1406DE7 120 | 121 | - Select the number from the list to import the public key 122 | 123 | - Verify the package against its PGP signature: 124 | 125 | .. code:: console 126 | 127 | gpg --verify robotframework-imaplibrary-x.x.x.tar.gz.asc robotframework-imaplibrary-x.x.x.tar.gz 128 | 129 | - Extract each source distribution to a temporary location. 130 | 131 | - Go to each created directory from the command line and install each project using: 132 | 133 | .. code:: console 134 | 135 | python setup.py install 136 | 137 | If you are on Windows, and there are Windows installers available for 138 | certain projects, you can use them instead of source distributions. 139 | Just download 32bit or 64bit installer depending on your system, 140 | double-click it, and follow the instructions. 141 | 142 | Directory Layout 143 | ---------------- 144 | 145 | doc/ 146 | `Keyword documentation`_ 147 | 148 | src/ 149 | Python source code 150 | 151 | test/ 152 | Test files 153 | 154 | utest/ 155 | Python unit test 156 | 157 | Usage 158 | ----- 159 | 160 | To write tests with Robot Framework and ImapLibrary, 161 | ImapLibrary must be imported into your Robot test suite. 162 | 163 | .. code:: robotframework 164 | 165 | *** Settings *** 166 | Library ImapLibrary 167 | 168 | See `Robot Framework User Guide`_ for more information. 169 | 170 | More information about Robot Framework standard libraries and built-in tools 171 | can be found in the `Robot Framework Documentation`_. 172 | 173 | Building Keyword Documentation 174 | ------------------------------ 175 | 176 | The `Keyword Documentation`_ can be found online, if you need to generate the keyword documentation, run: 177 | 178 | .. code:: console 179 | 180 | make doc 181 | 182 | Run Unit Tests, and Test Coverage Report 183 | ---------------------------------------- 184 | 185 | Test the testing library, talking about dogfooding, let's run: 186 | 187 | .. code:: console 188 | 189 | make test 190 | 191 | Contributing 192 | ------------ 193 | 194 | 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. 195 | 196 | 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. 197 | 198 | Before your code can be accepted into the project you must also sign the `Imap Library CLA`_ (Individual Contributor License Agreement). 199 | 200 | That's it! Thank you for your contribution! 201 | 202 | License 203 | ------- 204 | 205 | Copyright (c) 2015-2016 Richard Huang. 206 | 207 | This library is free software, licensed under: `Apache License, Version 2.0`_. 208 | 209 | Documentation and other similar content are provided under `Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License`_. 210 | 211 | .. _Apache License, Version 2.0: https://goo.gl/qpvnnB 212 | .. _CNTML: http://goo.gl/ukiwSO 213 | .. _Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License: http://goo.gl/SNw73V 214 | .. _Imap Library CLA: https://goo.gl/forms/QMyqXJI2LM 215 | .. _Keyword Documentation: https://goo.gl/ntRuxC 216 | .. _pip: http://goo.gl/jlJCPE 217 | .. _Robot Framework: http://goo.gl/lES6WM 218 | .. _Robot Framework Documentation: http://goo.gl/zy53tf 219 | .. _Robot Framework installed: https://goo.gl/PFbWqM 220 | .. _Robot Framework User Guide: http://goo.gl/Q7dfPB 221 | .. _robotframework-imaplibrary: https://goo.gl/q66LcA 222 | .. |Docs| image:: https://img.shields.io/badge/docs-latest-brightgreen.svg 223 | :target: https://goo.gl/ntRuxC 224 | :alt: Keyword Documentation 225 | .. |Version| image:: https://img.shields.io/pypi/v/robotframework-imaplibrary.svg 226 | :target: https://goo.gl/q66LcA 227 | :alt: Package Version 228 | .. |Status| image:: https://img.shields.io/pypi/status/robotframework-imaplibrary.svg 229 | :target: https://goo.gl/q66LcA 230 | :alt: Development Status 231 | .. |Python| image:: https://img.shields.io/pypi/pyversions/robotframework-imaplibrary.svg 232 | :target: https://goo.gl/sXzgao 233 | :alt: Python Version 234 | .. |Download| image:: https://img.shields.io/pypi/dm/robotframework-imaplibrary.svg 235 | :target: https://goo.gl/q66LcA 236 | :alt: Monthly Download 237 | .. |License| image:: https://img.shields.io/pypi/l/robotframework-imaplibrary.svg 238 | :target: https://goo.gl/qpvnnB 239 | :alt: License 240 | -------------------------------------------------------------------------------- /analytics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2015-2016 Richard Huang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | IMAP Library - a IMAP email testing library. 20 | """ 21 | 22 | from os.path import split 23 | from re import sub 24 | import sys 25 | 26 | 27 | def main(argv): 28 | """Adds analytics code into auto generated documentation.""" 29 | try: 30 | path = argv[0] 31 | except IndexError: 32 | print("analytics.py ") 33 | sys.exit(1) 34 | 35 | with open(path) as reader: 36 | content = reader.read() 37 | 38 | analytics = """""" % (split(path)[1]) 46 | 47 | content = sub(r"", analytics + "\n", content) 48 | 49 | with open(path, "w") as writer: 50 | writer.write(content) 51 | 52 | if __name__ == "__main__": 53 | main(sys.argv[1:]) 54 | -------------------------------------------------------------------------------- /doc/ImapLibrary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 160 | 173 | 196 | 247 | 441 | 445 | 457 | 470 | 473 | 474 | 475 | 476 | 477 |
478 |

Opening library documentation failed

479 |
    480 |
  • Verify that you have JavaScript enabled in your browser.
  • 481 |
  • Make sure you are using a modern enough browser. Firefox 3.5, IE 8, or equivalent is required, newer browsers are recommended.
  • 482 |
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • 483 |
484 |
485 | 486 | 490 | 491 | 690 | 691 | 736 | 737 | 756 | 757 | 768 | 769 | 780 | 781 | 797 | 798 | 820 | 821 | 822 | 830 | 831 | 839 | 840 | 841 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | # Regular expression matching correct method names 3 | method-rgx=[a-z_][a-z0-9_]{2,40}$ 4 | 5 | [flake8] 6 | ignore=E501 7 | 8 | [MESSAGES CONTROL] 9 | disable=locally-disabled 10 | 11 | [sdist] 12 | formats=gztar,zip 13 | 14 | [TYPECHECK] 15 | ignored-modules=urllib2 16 | 17 | [wheel] 18 | universal=1 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2015-2016 Richard Huang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | IMAP Library - a IMAP email testing library. 20 | """ 21 | 22 | # To use a consistent encoding 23 | import codecs 24 | from os.path import abspath, dirname, join 25 | # Always prefer setuptools over distutils 26 | from setuptools import setup, find_packages 27 | 28 | LIBRARY_NAME = 'ImapLibrary' 29 | CWD = abspath(dirname(__file__)) 30 | VERSION_PATH = join(CWD, 'src', LIBRARY_NAME, 'version.py') 31 | exec(compile(open(VERSION_PATH).read(), VERSION_PATH, 'exec')) 32 | 33 | with codecs.open(join(CWD, 'README.rst'), encoding='utf-8') as reader: 34 | LONG_DESCRIPTION = reader.read() 35 | 36 | setup( 37 | name='robotframework-%s' % LIBRARY_NAME.lower(), 38 | version=VERSION, # pylint: disable=undefined-variable # noqa 39 | description='A IMAP email testing library for Robot Framework', 40 | long_description=LONG_DESCRIPTION, 41 | url='https://github.com/rickypc/robotframework-%s' % LIBRARY_NAME.lower(), 42 | author='Richard Huang', 43 | author_email='rickypc@users.noreply.github.com', 44 | license='Apache License, Version 2.0', 45 | classifiers=[ 46 | 'Development Status :: 4 - Beta', 47 | 'Framework :: Robot Framework', 48 | 'Intended Audience :: Developers', 49 | 'License :: OSI Approved :: Apache Software License', 50 | 'Programming Language :: Python :: 2.7', 51 | 'Programming Language :: Python :: 3.5', 52 | 'Topic :: Software Development :: Testing', 53 | ], 54 | keywords='robot framework testing automation imap email mail softwaretesting', 55 | platforms='any', 56 | packages=find_packages('src'), 57 | package_dir={'': 'src'}, 58 | install_requires=['future', 'robotframework >= 2.6.0'] 59 | ) 60 | -------------------------------------------------------------------------------- /src/ImapLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2015-2016 Richard Huang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | IMAP Library - a IMAP email testing library. 20 | """ 21 | 22 | from email import message_from_string 23 | from imaplib import IMAP4, IMAP4_SSL 24 | from re import findall 25 | from time import sleep, time 26 | try: 27 | from urllib.request import urlopen 28 | except ImportError: 29 | from urllib2 import urlopen 30 | from builtins import str as ustr 31 | from ImapLibrary.version import get_version 32 | 33 | __version__ = get_version() 34 | 35 | 36 | class ImapLibrary(object): 37 | """ImapLibrary is an email testing library for [http://goo.gl/lES6WM|Robot Framework]. 38 | 39 | *Deprecated Keywords Warning* 40 | 41 | These keywords will be removed in the future 3 to 5 releases. 42 | | *Deprecated Keyword* | *Alternative Keyword* | 43 | | `Open Link From Mail` | `Open Link From Email` | 44 | | `Mark As Read` | `Mark All Emails As Read` | 45 | | `Wait For Mail` | `Wait For Email` | 46 | 47 | Example: 48 | | `Open Mailbox` | host=imap.domain.com | user=email@domain.com | password=secret | 49 | | ${LATEST} = | `Wait For Email` | sender=noreply@domain.com | timeout=300 | 50 | | ${HTML} = | `Open Link From Email` | ${LATEST} | | 51 | | `Should Contain` | ${HTML} | address has been updated | | 52 | | `Close Mailbox` | | | | 53 | 54 | Multipart Email Example: 55 | | `Open Mailbox` | host=imap.domain.com | user=email@domain.com | password=secret | 56 | | ${LATEST} = | `Wait For Email` | sender=noreply@domain.com | timeout=300 | 57 | | ${parts} = | `Walk Multipart Email` | ${LATEST} | | 58 | | :FOR | ${i} | IN RANGE | ${parts} | 59 | | \\ | `Walk Multipart Email` | ${LATEST} | | 60 | | \\ | ${ctype} = | `Get Multipart Content Type` | | 61 | | \\ | `Continue For Loop If` | '${ctype}' != 'text/html' | | 62 | | \\ | ${payload} = | `Get Multipart Payload` | decode=True | 63 | | \\ | `Should Contain` | ${payload} | your email | 64 | | \\ | ${HTML} = | `Open Link From Email` | ${LATEST} | 65 | | \\ | `Should Contain` | ${HTML} | Your email | 66 | | `Close Mailbox` | | | | 67 | """ 68 | 69 | PORT = 143 70 | PORT_SECURE = 993 71 | FOLDER = 'INBOX' 72 | ROBOT_LIBRARY_SCOPE = 'GLOBAL' 73 | ROBOT_LIBRARY_VERSION = __version__ 74 | 75 | def __init__(self): 76 | """ImapLibrary can be imported without argument. 77 | 78 | Examples: 79 | | = Keyword Definition = | = Description = | 80 | | Library `|` ImapLibrary | Initiate Imap library | 81 | """ 82 | self._email_index = None 83 | self._imap = None 84 | self._mails = [] 85 | self._mp_iter = None 86 | self._mp_msg = None 87 | self._part = None 88 | 89 | def close_mailbox(self): 90 | """Close IMAP email client session. 91 | 92 | Examples: 93 | | Close Mailbox | 94 | """ 95 | self._imap.close() 96 | 97 | def delete_all_emails(self): 98 | """Delete all emails. 99 | 100 | Examples: 101 | | Delete All Emails | 102 | """ 103 | for mail in self._mails: 104 | self.delete_email(mail) 105 | self._imap.expunge() 106 | 107 | def delete_email(self, email_index): 108 | """Delete email on given ``email_index``. 109 | 110 | Arguments: 111 | - ``email_index``: An email index to identity the email message. 112 | 113 | Examples: 114 | | Delete Email | INDEX | 115 | """ 116 | self._imap.uid('store', email_index, '+FLAGS', r'(\DELETED)') 117 | self._imap.expunge() 118 | 119 | def get_email_body(self, email_index): 120 | """Returns the decoded email body on multipart email message, 121 | otherwise returns the body text. 122 | 123 | Arguments: 124 | - ``email_index``: An email index to identity the email message. 125 | 126 | Examples: 127 | | Get Email Body | INDEX | 128 | """ 129 | if self._is_walking_multipart(email_index): 130 | body = self.get_multipart_payload(decode=True) 131 | else: 132 | body = self._imap.uid('fetch', 133 | email_index, 134 | '(BODY[TEXT])')[1][0][1].\ 135 | decode('quoted-printable') 136 | return body 137 | 138 | def get_links_from_email(self, email_index): 139 | """Returns all links found in the email body from given ``email_index``. 140 | 141 | Arguments: 142 | - ``email_index``: An email index to identity the email message. 143 | 144 | Examples: 145 | | Get Links From Email | INDEX | 146 | """ 147 | body = self.get_email_body(email_index) 148 | return findall(r'href=[\'"]?([^\'" >]+)', body) 149 | 150 | def get_matches_from_email(self, email_index, pattern): 151 | """Returns all Regular Expression ``pattern`` found in the email body 152 | from given ``email_index``. 153 | 154 | Arguments: 155 | - ``email_index``: An email index to identity the email message. 156 | - ``pattern``: It consists of one or more character literals, operators, or constructs. 157 | 158 | Examples: 159 | | Get Matches From Email | INDEX | PATTERN | 160 | """ 161 | body = self.get_email_body(email_index) 162 | return findall(pattern, body) 163 | 164 | def get_multipart_content_type(self): 165 | """Returns the content type of current part of selected multipart email message. 166 | 167 | Examples: 168 | | Get Multipart Content Type | 169 | """ 170 | return self._part.get_content_type() 171 | 172 | def get_multipart_field(self, field): 173 | """Returns the value of given header ``field`` name. 174 | 175 | Arguments: 176 | - ``field``: A header field name: ``From``, ``To``, ``Subject``, ``Date``, etc. 177 | All available header field names of an email message can be found by running 178 | `Get Multipart Field Names` keyword. 179 | 180 | Examples: 181 | | Get Multipart Field | Subject | 182 | """ 183 | return self._mp_msg[field] 184 | 185 | def get_multipart_field_names(self): 186 | """Returns all available header field names of selected multipart email message. 187 | 188 | Examples: 189 | | Get Multipart Field Names | 190 | """ 191 | return list(self._mp_msg.keys()) 192 | 193 | def get_multipart_payload(self, decode=False): 194 | """Returns the payload of current part of selected multipart email message. 195 | 196 | Arguments: 197 | - ``decode``: An indicator flag to decode the email message. (Default False) 198 | 199 | Examples: 200 | | Get Multipart Payload | 201 | | Get Multipart Payload | decode=True | 202 | """ 203 | payload = self._part.get_payload(decode=decode) 204 | charset = self._part.get_content_charset() 205 | if charset is not None: 206 | return payload.decode(charset) 207 | return payload 208 | 209 | def mark_all_emails_as_read(self): 210 | """Mark all received emails as read. 211 | 212 | Examples: 213 | | Mark All Emails As Read | 214 | """ 215 | for mail in self._mails: 216 | self._imap.uid('store', mail, '+FLAGS', r'\SEEN') 217 | 218 | def mark_as_read(self): 219 | """****DEPRECATED**** 220 | Shortcut to `Mark All Emails As Read`. 221 | """ 222 | self.mark_all_emails_as_read() 223 | 224 | def mark_email_as_read(self, email_index): 225 | """Mark email on given ``email_index`` as read. 226 | 227 | Arguments: 228 | - ``email_index``: An email index to identity the email message. 229 | 230 | Examples: 231 | | Mark Email As Read | INDEX | 232 | """ 233 | self._imap.uid('store', email_index, '+FLAGS', r'\SEEN') 234 | 235 | def open_link_from_email(self, email_index, link_index=0): 236 | """Open link URL from given ``link_index`` in email message body of given ``email_index``. 237 | Returns HTML content of opened link URL. 238 | 239 | Arguments: 240 | - ``email_index``: An email index to identity the email message. 241 | - ``link_index``: The link index to be open. (Default 0) 242 | 243 | Examples: 244 | | Open Link From Email | 245 | | Open Link From Email | 1 | 246 | """ 247 | urls = self.get_links_from_email(email_index) 248 | 249 | if len(urls) > link_index: 250 | resp = urlopen(urls[link_index]) 251 | content_type = resp.headers.getheader('content-type') 252 | if content_type: 253 | enc = content_type.split('charset=')[-1] 254 | return ustr(resp.read(), enc) 255 | else: 256 | return resp.read() 257 | else: 258 | raise AssertionError("Link number %i not found!" % link_index) 259 | 260 | def open_link_from_mail(self, email_index, link_index=0): 261 | """****DEPRECATED**** 262 | Shortcut to `Open Link From Email`. 263 | """ 264 | return self.open_link_from_email(email_index, link_index) 265 | 266 | def open_mailbox(self, **kwargs): 267 | """Open IMAP email client session to given ``host`` with given ``user`` and ``password``. 268 | 269 | Arguments: 270 | - ``host``: The IMAP host server. (Default None) 271 | - ``is_secure``: An indicator flag to connect to IMAP host securely or not. (Default True) 272 | - ``password``: The plaintext password to be use to authenticate mailbox on given ``host``. 273 | - ``port``: The IMAP port number. (Default None) 274 | - ``user``: The username to be use to authenticate mailbox on given ``host``. 275 | - ``folder``: The email folder to read from. (Default INBOX) 276 | 277 | Examples: 278 | | Open Mailbox | host=HOST | user=USER | password=SECRET | 279 | | Open Mailbox | host=HOST | user=USER | password=SECRET | is_secure=False | 280 | | Open Mailbox | host=HOST | user=USER | password=SECRET | port=8000 | 281 | | Open Mailbox | host=HOST | user=USER | password=SECRET | folder=Drafts 282 | """ 283 | host = kwargs.pop('host', kwargs.pop('server', None)) 284 | is_secure = kwargs.pop('is_secure', 'True') == 'True' 285 | port = int(kwargs.pop('port', self.PORT_SECURE if is_secure else self.PORT)) 286 | folder = '"%s"' % str(kwargs.pop('folder', self.FOLDER)) 287 | self._imap = IMAP4_SSL(host, port) if is_secure else IMAP4(host, port) 288 | self._imap.login(kwargs.pop('user', None), kwargs.pop('password', None)) 289 | self._imap.select(folder) 290 | self._init_multipart_walk() 291 | 292 | def wait_for_email(self, **kwargs): 293 | """Wait for email message to arrived base on any given filter criteria. 294 | Returns email index of the latest email message received. 295 | 296 | Arguments: 297 | - ``poll_frequency``: The delay value in seconds to retry the mailbox check. (Default 10) 298 | - ``recipient``: Email recipient. (Default None) 299 | - ``sender``: Email sender. (Default None) 300 | - ``status``: A mailbox status filter: ``MESSAGES``, ``RECENT``, ``UIDNEXT``, 301 | ``UIDVALIDITY``, and ``UNSEEN``. 302 | Please see [https://goo.gl/3KKHoY|Mailbox Status] for more information. 303 | (Default None) 304 | - ``subject``: Email subject. (Default None) 305 | - ``text``: Email body text. (Default None) 306 | - ``timeout``: The maximum value in seconds to wait for email message to arrived. 307 | (Default 60) 308 | - ``folder``: The email folder to check for emails. (Default INBOX) 309 | 310 | Examples: 311 | | Wait For Email | sender=noreply@domain.com | 312 | | Wait For Email | sender=noreply@domain.com | folder=OUTBOX 313 | """ 314 | poll_frequency = float(kwargs.pop('poll_frequency', 10)) 315 | timeout = int(kwargs.pop('timeout', 60)) 316 | end_time = time() + timeout 317 | while time() < end_time: 318 | self._mails = self._check_emails(**kwargs) 319 | if len(self._mails) > 0: 320 | return self._mails[-1] 321 | if time() < end_time: 322 | sleep(poll_frequency) 323 | raise AssertionError("No email received within %ss" % timeout) 324 | 325 | def wait_for_mail(self, **kwargs): 326 | """****DEPRECATED**** 327 | Shortcut to `Wait For Email`. 328 | """ 329 | return self.wait_for_email(**kwargs) 330 | 331 | def walk_multipart_email(self, email_index): 332 | """Returns total parts of a multipart email message on given ``email_index``. 333 | Email message is cache internally to be used by other multipart keywords: 334 | `Get Multipart Content Type`, `Get Multipart Field`, `Get Multipart Field Names`, 335 | `Get Multipart Field`, and `Get Multipart Payload`. 336 | 337 | Arguments: 338 | - ``email_index``: An email index to identity the email message. 339 | 340 | Examples: 341 | | Walk Multipart Email | INDEX | 342 | """ 343 | if not self._is_walking_multipart(email_index): 344 | data = self._imap.uid('fetch', email_index, '(RFC822)')[1][0][1] 345 | msg = message_from_string(data) 346 | self._start_multipart_walk(email_index, msg) 347 | try: 348 | self._part = next(self._mp_iter) 349 | except StopIteration: 350 | self._init_multipart_walk() 351 | return False 352 | # return number of parts 353 | return len(self._mp_msg.get_payload()) 354 | 355 | def _check_emails(self, **kwargs): 356 | """Returns filtered email.""" 357 | folder = '"%s"' % str(kwargs.pop('folder', self.FOLDER)) 358 | criteria = self._criteria(**kwargs) 359 | # Calling select before each search is necessary with gmail 360 | status, data = self._imap.select(folder) 361 | if status != 'OK': 362 | raise Exception("imap.select error: %s, %s" % (status, data)) 363 | typ, msgnums = self._imap.uid('search', None, *criteria) 364 | if typ != 'OK': 365 | raise Exception('imap.search error: %s, %s, criteria=%s' % (typ, msgnums, criteria)) 366 | return msgnums[0].split() 367 | 368 | @staticmethod 369 | def _criteria(**kwargs): 370 | """Returns email criteria.""" 371 | criteria = [] 372 | recipient = kwargs.pop('recipient', kwargs.pop('to_email', kwargs.pop('toEmail', None))) 373 | sender = kwargs.pop('sender', kwargs.pop('from_email', kwargs.pop('fromEmail', None))) 374 | status = kwargs.pop('status', None) 375 | subject = kwargs.pop('subject', None) 376 | text = kwargs.pop('text', None) 377 | if recipient: 378 | criteria += ['TO', '"%s"' % recipient] 379 | if sender: 380 | criteria += ['FROM', '"%s"' % sender] 381 | if subject: 382 | criteria += ['SUBJECT', '"%s"' % subject] 383 | if text: 384 | criteria += ['TEXT', '"%s"' % text] 385 | if status: 386 | criteria += [status] 387 | if not criteria: 388 | criteria = ['UNSEEN'] 389 | return criteria 390 | 391 | def _init_multipart_walk(self): 392 | """Initialize multipart email walk.""" 393 | self._email_index = None 394 | self._mp_msg = None 395 | self._part = None 396 | 397 | def _is_walking_multipart(self, email_index): 398 | """Returns boolean value whether the multipart email walk is in-progress or not.""" 399 | return self._mp_msg is not None and self._email_index == email_index 400 | 401 | def _start_multipart_walk(self, email_index, msg): 402 | """Start multipart email walk.""" 403 | self._email_index = email_index 404 | self._mp_msg = msg 405 | self._mp_iter = msg.walk() 406 | -------------------------------------------------------------------------------- /src/ImapLibrary/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2015-2016 Richard Huang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | IMAP Library - a IMAP email testing library. 20 | """ 21 | 22 | VERSION = '0.3.0' 23 | 24 | 25 | def get_version(): 26 | """Returns the current version.""" 27 | return VERSION 28 | -------------------------------------------------------------------------------- /test/utest/test_imaplibrary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2015-2016 Richard Huang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | IMAP Library - a IMAP email testing library. 20 | """ 21 | 22 | from sys import path 23 | path.append('src') 24 | from ImapLibrary import ImapLibrary 25 | import mock 26 | import unittest 27 | 28 | 29 | class ImapLibraryTests(unittest.TestCase): 30 | """Imap library test class.""" 31 | 32 | def setUp(self): 33 | """Instantiate the Imap library class.""" 34 | self.library = ImapLibrary() 35 | self.password = 'password' 36 | self.port = 143 37 | self.port_secure = 993 38 | self.recipient = 'my@domain.com' 39 | self.sender = 'noreply@domain.com' 40 | self.server = 'my.imap' 41 | self.status = 'UNSEEN' 42 | self.subject = 'subject' 43 | self.text = 'text' 44 | self.username = 'username' 45 | self.folder = 'INBOX' 46 | self.folder_check = '"INBOX"' 47 | self.folder_filter = '"OUTBOX"' 48 | 49 | def test_should_have_default_values(self): 50 | """Imap library instance should have default values set.""" 51 | self.assertIsInstance(self.library, ImapLibrary) 52 | self.assertIsNone(self.library._email_index) 53 | self.assertIsNone(self.library._imap) 54 | self.assertIsInstance(self.library._mails, list) 55 | self.assertIsNone(self.library._mp_iter) 56 | self.assertIsNone(self.library._mp_msg) 57 | self.assertIsNone(self.library._part) 58 | self.assertEqual(self.library.PORT, self.port) 59 | self.assertEqual(self.library.PORT_SECURE, self.port_secure) 60 | self.assertEqual(self.library.FOLDER, self.folder) 61 | 62 | @mock.patch('ImapLibrary.IMAP4_SSL') 63 | def test_should_open_secure_mailbox(self, mock_imap): 64 | """Open mailbox should open secure connection to IMAP server 65 | with requested credentials. 66 | """ 67 | self.library.open_mailbox(host=self.server, user=self.username, 68 | password=self.password) 69 | mock_imap.assert_called_with(self.server, self.port_secure) 70 | self.library._imap.login.assert_called_with(self.username, self.password) 71 | self.library._imap.select.assert_called_with(self.folder_check) 72 | 73 | @mock.patch('ImapLibrary.IMAP4_SSL') 74 | def test_should_open_secure_mailbox_with_custom_port(self, mock_imap): 75 | """Open mailbox should open secure connection to IMAP server 76 | with requested credentials and custom port. 77 | """ 78 | self.library.open_mailbox(host=self.server, user=self.username, 79 | password=self.password, port=8000) 80 | mock_imap.assert_called_with(self.server, 8000) 81 | self.library._imap.login.assert_called_with(self.username, self.password) 82 | self.library._imap.select.assert_called_with(self.folder_check) 83 | 84 | @mock.patch('ImapLibrary.IMAP4_SSL') 85 | def test_should_open_secure_mailbox_with_custom_folder(self, mock_imap): 86 | """Open mailbox should open secure connection to IMAP server with 87 | requested credentials to a custom folder 88 | """ 89 | self.library.open_mailbox(host=self.server, user=self.username, 90 | password=self.password, folder='Outbox') 91 | mock_imap.assert_called_with(self.server, self.port_secure) 92 | self.library._imap.login.assert_called_with(self.username, self.password) 93 | self.library._imap.select.assert_called_with('"Outbox"') 94 | 95 | @mock.patch('ImapLibrary.IMAP4_SSL') 96 | def test_should_open_secure_mailbox_with_server_key(self, mock_imap): 97 | """Open mailbox should open secure connection to IMAP server 98 | using 'server' key with requested credentials. 99 | """ 100 | self.library.open_mailbox(server=self.server, user=self.username, 101 | password=self.password) 102 | mock_imap.assert_called_with(self.server, self.port_secure) 103 | self.library._imap.login.assert_called_with(self.username, self.password) 104 | self.library._imap.select.assert_called_with(self.folder_check) 105 | 106 | @mock.patch('ImapLibrary.IMAP4') 107 | def test_should_open_non_secure_mailbox(self, mock_imap): 108 | """Open mailbox should open non-secure connection to IMAP server 109 | with requested credentials. 110 | """ 111 | self.library.open_mailbox(host=self.server, user=self.username, 112 | password=self.password, is_secure=False) 113 | mock_imap.assert_called_with(self.server, self.port) 114 | self.library._imap.login.assert_called_with(self.username, self.password) 115 | self.library._imap.select.assert_called_with(self.folder_check) 116 | 117 | @mock.patch('ImapLibrary.IMAP4_SSL') 118 | def test_should_return_email_index(self, mock_imap): 119 | """Returns email index from connected IMAP session.""" 120 | self.library.open_mailbox(host=self.server, user=self.username, 121 | password=self.password) 122 | self.library._imap.select.return_value = ['OK', ['1']] 123 | self.library._imap.uid.return_value = ['OK', ['0']] 124 | index = self.library.wait_for_email(sender=self.sender) 125 | self.library._imap.select.assert_called_with(self.folder_check) 126 | self.library._imap.uid.assert_called_with('search', None, 'FROM', 127 | '"%s"' % self.sender) 128 | self.assertEqual(index, '0') 129 | 130 | @mock.patch('ImapLibrary.IMAP4_SSL') 131 | def test_should_return_email_index_with_sender_filter(self, mock_imap): 132 | """Returns email index from connected IMAP session 133 | with sender filter. 134 | """ 135 | self.library.open_mailbox(host=self.server, user=self.username, 136 | password=self.password) 137 | self.library._imap.select.return_value = ['OK', ['1']] 138 | self.library._imap.uid.return_value = ['OK', ['0']] 139 | index = self.library.wait_for_email(sender=self.sender) 140 | self.library._imap.select.assert_called_with(self.folder_check) 141 | self.library._imap.uid.assert_called_with('search', None, 'FROM', 142 | '"%s"' % self.sender) 143 | self.assertEqual(index, '0') 144 | index = self.library.wait_for_email(from_email=self.sender) 145 | self.library._imap.select.assert_called_with(self.folder_check) 146 | self.library._imap.uid.assert_called_with('search', None, 'FROM', 147 | '"%s"' % self.sender) 148 | self.assertEqual(index, '0') 149 | index = self.library.wait_for_email(fromEmail=self.sender) 150 | self.library._imap.select.assert_called_with(self.folder_check) 151 | self.library._imap.uid.assert_called_with('search', None, 'FROM', 152 | '"%s"' % self.sender) 153 | self.assertEqual(index, '0') 154 | 155 | @mock.patch('ImapLibrary.IMAP4_SSL') 156 | def test_should_return_email_index_with_recipient_filter(self, mock_imap): 157 | """Returns email index from connected IMAP session 158 | with recipient filter. 159 | """ 160 | self.library.open_mailbox(host=self.server, user=self.username, 161 | password=self.password) 162 | self.library._imap.select.return_value = ['OK', ['1']] 163 | self.library._imap.uid.return_value = ['OK', ['0']] 164 | index = self.library.wait_for_email(recipient=self.recipient) 165 | self.library._imap.select.assert_called_with(self.folder_check) 166 | self.library._imap.uid.assert_called_with('search', None, 'TO', 167 | '"%s"' % self.recipient) 168 | self.assertEqual(index, '0') 169 | index = self.library.wait_for_email(to_email=self.recipient) 170 | self.library._imap.select.assert_called_with(self.folder_check) 171 | self.library._imap.uid.assert_called_with('search', None, 'TO', 172 | '"%s"' % self.recipient) 173 | self.assertEqual(index, '0') 174 | index = self.library.wait_for_email(toEmail=self.recipient) 175 | self.library._imap.select.assert_called_with(self.folder_check) 176 | self.library._imap.uid.assert_called_with('search', None, 'TO', 177 | '"%s"' % self.recipient) 178 | self.assertEqual(index, '0') 179 | 180 | @mock.patch('ImapLibrary.IMAP4_SSL') 181 | def test_should_return_email_index_with_subject_filter(self, mock_imap): 182 | """Returns email index from connected IMAP session 183 | with subject filter. 184 | """ 185 | self.library.open_mailbox(host=self.server, user=self.username, 186 | password=self.password) 187 | self.library._imap.select.return_value = ['OK', ['1']] 188 | self.library._imap.uid.return_value = ['OK', ['0']] 189 | index = self.library.wait_for_email(subject=self.subject) 190 | self.library._imap.select.assert_called_with(self.folder_check) 191 | self.library._imap.uid.assert_called_with('search', None, 'SUBJECT', 192 | '"%s"' % self.subject) 193 | self.assertEqual(index, '0') 194 | 195 | @mock.patch('ImapLibrary.IMAP4_SSL') 196 | def test_should_return_email_index_with_text_filter(self, mock_imap): 197 | """Returns email index from connected IMAP session with text filter.""" 198 | self.library.open_mailbox(host=self.server, user=self.username, 199 | password=self.password) 200 | self.library._imap.select.return_value = ['OK', ['1']] 201 | self.library._imap.uid.return_value = ['OK', ['0']] 202 | index = self.library.wait_for_email(text=self.text) 203 | self.library._imap.select.assert_called_with(self.folder_check) 204 | self.library._imap.uid.assert_called_with('search', None, 'TEXT', 205 | '"%s"' % self.text) 206 | self.assertEqual(index, '0') 207 | 208 | @mock.patch('ImapLibrary.IMAP4_SSL') 209 | def test_should_return_email_index_with_status_filter(self, mock_imap): 210 | """Returns email index from connected IMAP session with status filter.""" 211 | self.library.open_mailbox(host=self.server, user=self.username, 212 | password=self.password) 213 | self.library._imap.select.return_value = ['OK', ['1']] 214 | self.library._imap.uid.return_value = ['OK', ['0']] 215 | index = self.library.wait_for_email(status=self.status) 216 | self.library._imap.select.assert_called_with(self.folder_check) 217 | self.library._imap.uid.assert_called_with('search', None, self.status) 218 | self.assertEqual(index, '0') 219 | 220 | @mock.patch('ImapLibrary.IMAP4_SSL') 221 | def test_should_return_email_index_with_folder_filter(self, mock_imap): 222 | """Returns email index from connected IMAP session with 223 | folder filter. 224 | """ 225 | self.library.open_mailbox(host=self.server, user=self.username, 226 | password=self.password) 227 | self.library._imap.select.return_value = ['OK', ['1']] 228 | self.library._imap.uid.return_value = ['OK', ['0']] 229 | index = self.library.wait_for_email(folder='OUTBOX') 230 | self.library._imap.select.return_value = ['OK', ['1']] 231 | self.library._imap.uid.return_value = ['OK', ['0']] 232 | self.library._imap.select.assert_called_with(self.folder_filter) 233 | self.assertEqual(index, '0') 234 | 235 | @mock.patch('ImapLibrary.IMAP4_SSL') 236 | def test_should_return_email_index_without_filter(self, mock_imap): 237 | """Returns email index from connected IMAP session without filter.""" 238 | self.library.open_mailbox(host=self.server, user=self.username, 239 | password=self.password) 240 | self.library._imap.select.return_value = ['OK', ['1']] 241 | self.library._imap.uid.return_value = ['OK', ['0']] 242 | index = self.library.wait_for_email() 243 | self.library._imap.select.assert_called_with(self.folder_check) 244 | self.library._imap.uid.assert_called_with('search', None, self.status) 245 | self.assertEqual(index, '0') 246 | 247 | # DEPRECATED 248 | @mock.patch('ImapLibrary.IMAP4_SSL') 249 | def test_should_return_email_index_from_deprecated_keyword(self, mock_imap): 250 | """Returns email index from connected IMAP session 251 | using deprecated keyword. 252 | """ 253 | self.library.open_mailbox(host=self.server, user=self.username, 254 | password=self.password) 255 | self.library._imap.select.return_value = ['OK', ['1']] 256 | self.library._imap.uid.return_value = ['OK', ['0']] 257 | index = self.library.wait_for_mail(sender=self.sender) 258 | self.library._imap.select.assert_called_with(self.folder_check) 259 | self.library._imap.uid.assert_called_with('search', None, 'FROM', 260 | '"%s"' % self.sender) 261 | self.assertEqual(index, '0') 262 | 263 | @mock.patch('ImapLibrary.IMAP4_SSL') 264 | def test_should_return_email_index_after_delay(self, mock_imap): 265 | """Returns email index from connected IMAP session after some delay.""" 266 | self.library.open_mailbox(host=self.server, user=self.username, 267 | password=self.password) 268 | self.library._imap.select.return_value = ['OK', ['1']] 269 | self.library._imap.uid.side_effect = [['OK', ['']], ['OK', ['0']]] 270 | index = self.library.wait_for_email(sender=self.sender, poll_frequency=0.2) 271 | self.library._imap.select.assert_called_with(self.folder_check) 272 | self.library._imap.uid.assert_called_with('search', None, 'FROM', 273 | '"%s"' % self.sender) 274 | self.assertEqual(index, '0') 275 | 276 | @mock.patch('ImapLibrary.IMAP4_SSL') 277 | def test_should_raise_exception_on_timeout(self, mock_imap): 278 | """Raise exception on timeout.""" 279 | self.library.open_mailbox(host=self.server, user=self.username, 280 | password=self.password) 281 | self.library._imap.select.return_value = ['OK', ['1']] 282 | self.library._imap.uid.return_value = ['OK', ['']] 283 | with self.assertRaises(AssertionError) as context: 284 | self.library.wait_for_email(sender=self.sender, poll_frequency=0.2, 285 | timeout=0.3) 286 | self.assertTrue("No email received within 0s" in context.exception) 287 | self.library._imap.select.assert_called_with(self.folder_check) 288 | 289 | @mock.patch('ImapLibrary.IMAP4_SSL') 290 | def test_should_raise_exception_on_select_error(self, mock_imap): 291 | """Raise exception on imap select error.""" 292 | self.library.open_mailbox(host=self.server, user=self.username, 293 | password=self.password) 294 | self.library._imap.select.return_value = ['NOK', ['1']] 295 | with self.assertRaises(Exception) as context: 296 | self.library.wait_for_email(sender=self.sender) 297 | self.assertTrue("imap.select error: NOK, ['1']" in context.exception) 298 | self.library._imap.select.assert_called_with(self.folder_check) 299 | 300 | @mock.patch('ImapLibrary.IMAP4_SSL') 301 | def test_should_raise_exception_on_search_error(self, mock_imap): 302 | """Raise exception on imap search error.""" 303 | self.library.open_mailbox(host=self.server, user=self.username, 304 | password=self.password) 305 | self.library._imap.select.return_value = ['OK', ['1']] 306 | self.library._imap.uid.return_value = ['NOK', ['']] 307 | with self.assertRaises(Exception) as context: 308 | self.library.wait_for_email(sender=self.sender) 309 | self.assertTrue("imap.search error: NOK, [''], criteria=['FROM', '%s']" % 310 | self.sender in context.exception) 311 | self.library._imap.select.assert_called_with(self.folder_check) 312 | self.library._imap.uid.assert_called_with('search', None, 'FROM', '"%s"' % 313 | self.sender) 314 | 315 | @mock.patch('ImapLibrary.IMAP4_SSL') 316 | def test_should_delete_all_emails(self, mock_imap): 317 | """Delete all emails.""" 318 | self.library.open_mailbox(host=self.server, user=self.username, 319 | password=self.password) 320 | self.library._mails = ['0'] 321 | self.library.delete_all_emails() 322 | self.library._imap.uid.assert_called_with('store', '0', '+FLAGS', r'(\DELETED)') 323 | self.library._imap.expunge.assert_called_with() 324 | 325 | @mock.patch('ImapLibrary.IMAP4_SSL') 326 | def test_should_delete_email(self, mock_imap): 327 | """Delete specific email.""" 328 | self.library.open_mailbox(host=self.server, user=self.username, 329 | password=self.password) 330 | self.library.delete_email('0') 331 | self.library._imap.uid.assert_called_with('store', '0', '+FLAGS', r'(\DELETED)') 332 | self.library._imap.expunge.assert_called_with() 333 | 334 | @mock.patch('ImapLibrary.IMAP4_SSL') 335 | def test_should_mark_all_emails_as_read(self, mock_imap): 336 | """Mark all emails as read.""" 337 | self.library.open_mailbox(host=self.server, user=self.username, 338 | password=self.password) 339 | self.library._mails = ['0'] 340 | self.library.mark_all_emails_as_read() 341 | self.library._imap.uid.assert_called_with('store', '0', '+FLAGS', r'\SEEN') 342 | 343 | # DEPRECATED 344 | @mock.patch('ImapLibrary.IMAP4_SSL') 345 | def test_should_mark_all_emails_as_read_from_deprecated_keyword(self, mock_imap): 346 | """Mark all emails as read using deprecated keyword.""" 347 | self.library.open_mailbox(host=self.server, user=self.username, 348 | password=self.password) 349 | self.library._mails = ['0'] 350 | self.library.mark_as_read() 351 | self.library._imap.uid.assert_called_with('store', '0', '+FLAGS', r'\SEEN') 352 | 353 | @mock.patch('ImapLibrary.IMAP4_SSL') 354 | def test_should_mark_email_as_read(self, mock_imap): 355 | """Mark specific email as read.""" 356 | self.library.open_mailbox(host=self.server, user=self.username, 357 | password=self.password) 358 | self.library.mark_email_as_read('0') 359 | self.library._imap.uid.assert_called_with('store', '0', '+FLAGS', r'\SEEN') 360 | 361 | @mock.patch('ImapLibrary.IMAP4_SSL') 362 | def test_should_close_mailbox(self, mock_imap): 363 | """Close opened connection.""" 364 | self.library.open_mailbox(host=self.server, user=self.username, 365 | password=self.password) 366 | self.library.close_mailbox() 367 | self.library._imap.close.assert_called_with() 368 | --------------------------------------------------------------------------------