Changeset 67
- Timestamp:
- 11/07/07 09:23:09 (1 year ago)
- Location:
- trunk
- Files:
-
- 13 modified
-
setup.py (modified) (1 diff)
-
tests/basic_tests.py (modified) (2 diffs)
-
tests/lib/smtp_mailsink.py (modified) (4 diffs)
-
tests/lib/utils.py (modified) (2 diffs)
-
tests/test_errorhandling.py (modified) (2 diffs)
-
tests/test_message_as_string.py (modified) (3 diffs)
-
tests/test_testmode.py (modified) (4 diffs)
-
turbomail/__init__.py (modified) (3 diffs)
-
turbomail/dispatch.py (modified) (1 diff)
-
turbomail/exceptions.py (modified) (1 diff)
-
turbomail/message.py (modified) (1 diff)
-
turbomail/pool.py (modified) (1 diff)
-
turbomail/startup.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
trunk/setup.py
r16 r67 13 13 14 14 setup( 15 name="TurboMail", 16 version=version, 17 18 description=description, 19 long_description=long_description, 20 author=author, 21 author_email=email, 22 url=url, 23 download_url=download_url, 24 license=license, 25 26 install_requires = ["TurboGears >= 0.9a9dev-r2003"], 27 zip_safe=True, 28 packages=find_packages(), 29 package_data = find_package_data(where='turbomail', package='turbomail'), 30 keywords = ["turbogears.extension"], 31 classifiers = [ 32 'Development Status :: 5 - Production/Stable', 33 'Framework :: TurboGears', 34 'Intended Audience :: Developers', 35 'License :: OSI Approved :: MIT License', 36 'Operating System :: OS Independent', 37 'Programming Language :: Python', 38 'Topic :: Communications :: Email', 39 'Topic :: Software Development :: Libraries :: Python Modules', 40 ], 41 test_suite = 'nose.collector', 42 entry_points = { 43 # 'paste.paster_create_template': ["turbomail = turbomail.startup:MailTemplate"] 44 'turbogears.extensions': ["turbomail = turbomail"] 45 } 46 ) 47 15 name="TurboMail", 16 version=version, 17 18 description=description, 19 long_description=long_description, 20 author=author, 21 author_email=email, 22 url=url, 23 download_url=download_url, 24 license=license, 25 26 install_requires = ["TurboGears >= 0.9a9dev-r2003"], 27 zip_safe=True, 28 packages=find_packages(), 29 package_data = find_package_data(where='turbomail', package='turbomail'), 30 keywords = ["turbogears.extension"], 31 classifiers = [ 32 'Development Status :: 5 - Production/Stable', 33 'Framework :: TurboGears', 34 'Intended Audience :: Developers', 35 'License :: OSI Approved :: MIT License', 36 'Operating System :: OS Independent', 37 'Programming Language :: Python', 38 'Topic :: Communications :: Email', 39 'Topic :: Software Development :: Libraries :: Python Modules', 40 ], 41 test_suite = 'nose.collector', 42 entry_points = { 43 # 'paste.paster_create_template': ["turbomail = turbomail.startup:MailTemplate"] 44 'turbogears.extensions': ["turbomail = turbomail"] 45 } 46 ) -
trunk/tests/basic_tests.py
r47 r67 6 6 # This code is placed under the MIT license: 7 7 # 8 # Permission is hereby granted, free of charge, to any person obtaining a copy 9 # of this software and associated documentation files (the "Software"), to deal 10 # in the Software without restriction, including without limitation the rights 11 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 # copies of the Software, and to permit persons to whom the Software is 8 # Permission is hereby granted, free of charge, to any person obtaining a copy 9 # of this software and associated documentation files (the "Software"), to deal 10 # in the Software without restriction, including without limitation the rights 11 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 # copies of the Software, and to permit persons to whom the Software is 13 13 # furnished to do so, subject to the following conditions: 14 14 # 15 # The above copyright notice and this permission notice shall be included in 15 # The above copyright notice and this permission notice shall be included in 16 16 # all copies or substantial portions of the Software. 17 17 # 18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 24 # SOFTWARE. 25 25 … … 38 38 39 39 class TestBasicTests(unittest.TestCase): 40 "Basic test cases for TurboMail."40 "Basic test cases for TurboMail." 41 41 42 def setUp(self):43 server_port = 4204244 45 test_config = {'mail.on': True, 'mail.timeout': 1,46 'mail.server': 'localhost:%d' % server_port}47 self._original_config = save_config(test_config.keys())48 turbogears.config.update(test_config)49 50 self.sink = SMTPMailsink(host='localhost', port=server_port)51 self.sink.start()52 53 turbomail.start_extension()42 def setUp(self): 43 server_port = 42042 44 45 test_config = {'mail.on': True, 'mail.timeout': 1, 46 'mail.server': 'localhost:%d' % server_port} 47 self._original_config = save_config(test_config.keys()) 48 turbogears.config.update(test_config) 49 50 self.sink = SMTPMailsink(host='localhost', port=server_port) 51 self.sink.start() 52 53 turbomail.start_extension() 54 54 55 55 56 def tearDown(self):57 turbomail.shutdown_extension()58 turbogears.config.update(self._original_config)59 self.sink.stop()56 def tearDown(self): 57 turbomail.shutdown_extension() 58 turbogears.config.update(self._original_config) 59 self.sink.stop() 60 60 61 61 62 def test_simple(self):63 "Test that sending a simple mail with turbomail.Message works."64 sender = 'sender@foo.example'65 recipient = 'recipient@foo.example'66 subject = 'foo bar'67 message = turbomail.Message(sender, recipient, subject)68 message.plain = 'Hello World!' 69 turbomail.enqueue(message)62 def test_simple(self): 63 "Test that sending a simple mail with turbomail.Message works." 64 sender = 'sender@foo.example' 65 recipient = 'recipient@foo.example' 66 subject = 'foo bar' 67 message = turbomail.Message(sender, recipient, subject) 68 message.plain = 'Hello World!' 69 turbomail.enqueue(message) 70 70 71 msginfo = get_received_mail(self.sink)72 self.assertEqual(sender, msginfo['from'])73 self.assertEqual([recipient], msginfo['recipients'])74 msg = email.message_from_string(msginfo['mail']) 75 self.failIf(msg.has_key('Old-Return-Path'))76 self.failIf(msg.has_key('Return-Path'))77 assert 'Hello World' in msg.get_payload() 71 msginfo = get_received_mail(self.sink) 72 self.assertEqual(sender, msginfo['from']) 73 self.assertEqual([recipient], msginfo['recipients']) 74 msg = email.message_from_string(msginfo['mail']) 75 self.failIf(msg.has_key('Old-Return-Path')) 76 self.failIf(msg.has_key('Return-Path')) 77 assert 'Hello World' in msg.get_payload() 78 78 79 def test_smtpfrom(self):80 "Test that smtpfrom is being honored and used as envelope sender."81 sender = 'sender@foo.example'82 recipient = 'recipient@foo.example'83 subject = 'foo bar'84 smtpfrom = 'devnull@foo.example'85 message = turbomail.Message(sender, recipient, subject, 86 smtpfrom=smtpfrom)87 message.plain = 'Hello World!' 88 turbomail.enqueue(message)89 msginfo = get_received_mail(self.sink)90 self.assertEqual(smtpfrom, msginfo['from'])91 self.assertEqual([recipient], msginfo['recipients'])79 def test_smtpfrom(self): 80 "Test that smtpfrom is being honored and used as envelope sender." 81 sender = 'sender@foo.example' 82 recipient = 'recipient@foo.example' 83 subject = 'foo bar' 84 smtpfrom = 'devnull@foo.example' 85 message = turbomail.Message(sender, recipient, subject, 86 smtpfrom=smtpfrom) 87 message.plain = 'Hello World!' 88 turbomail.enqueue(message) 89 msginfo = get_received_mail(self.sink) 90 self.assertEqual(smtpfrom, msginfo['from']) 91 self.assertEqual([recipient], msginfo['recipients']) 92 92 93 def test_add_custom_headers_dict(self):94 "Test that custom headers (dict type) can be attached."95 extra_headers = {'Precendence': 'bulk', 'X-User': 'Alice'}96 message = turbomail.Message('sender@foo.example', 97 'recipient@foo.example', 'foo bar')98 message.plain = 'Hello World!'99 message.headers = extra_headers100 turbomail.enqueue(message)101 msginfo = get_received_mail(self.sink)102 msg = email.message_from_string(msginfo['mail']) 103 for header_name in extra_headers.keys():104 self.failUnless(msg.has_key(header_name))105 self.assertEquals(extra_headers[header_name], msg[header_name])93 def test_add_custom_headers_dict(self): 94 "Test that custom headers (dict type) can be attached." 95 extra_headers = {'Precendence': 'bulk', 'X-User': 'Alice'} 96 message = turbomail.Message('sender@foo.example', 97 'recipient@foo.example', 'foo bar') 98 message.plain = 'Hello World!' 99 message.headers = extra_headers 100 turbomail.enqueue(message) 101 msginfo = get_received_mail(self.sink) 102 msg = email.message_from_string(msginfo['mail']) 103 for header_name in extra_headers.keys(): 104 self.failUnless(msg.has_key(header_name)) 105 self.assertEquals(extra_headers[header_name], msg[header_name]) 106 106 107 def test_add_custom_headers_tuple(self):108 "Test that a custom header (tuple type) can be attached."109 extra_headers = (('Precendence', 'bulk'), ('X-User', 'Alice'))110 message = turbomail.Message('sender@foo.example', 111 'recipient@foo.example', 'foo bar')112 message.plain = 'Hello World!'113 message.headers = extra_headers114 turbomail.enqueue(message)115 msginfo = get_received_mail(self.sink)116 msg = email.message_from_string(msginfo['mail']) 117 for name, value in extra_headers:118 self.failUnless(msg.has_key(name))119 self.assertEquals(value, msg[name])107 def test_add_custom_headers_tuple(self): 108 "Test that a custom header (tuple type) can be attached." 109 extra_headers = (('Precendence', 'bulk'), ('X-User', 'Alice')) 110 message = turbomail.Message('sender@foo.example', 111 'recipient@foo.example', 'foo bar') 112 message.plain = 'Hello World!' 113 message.headers = extra_headers 114 turbomail.enqueue(message) 115 msginfo = get_received_mail(self.sink) 116 msg = email.message_from_string(msginfo['mail']) 117 for name, value in extra_headers: 118 self.failUnless(msg.has_key(name)) 119 self.assertEquals(value, msg[name]) 120 120 121 def test_add_custom_headers_list(self): 122 "Test that a custom header (list type) can be attached." 123 extra_headers = [('Precendence', 'bulk'), ('X-User', 'Alice')] 124 message = turbomail.Message('sender@foo.example', 125 'recipient@foo.example', 'foo bar') 126 message.plain = 'Hello World!' 127 message.headers = extra_headers 128 turbomail.enqueue(message) 129 msginfo = get_received_mail(self.sink) 130 msg = email.message_from_string(msginfo['mail']) 131 for name, value in extra_headers: 132 self.failUnless(msg.has_key(name)) 133 self.assertEquals(value, msg[name]) 134 121 def test_add_custom_headers_list(self): 122 "Test that a custom header (list type) can be attached." 123 extra_headers = [('Precendence', 'bulk'), ('X-User', 'Alice')] 124 message = turbomail.Message('sender@foo.example', 125 'recipient@foo.example', 'foo bar') 126 message.plain = 'Hello World!' 127 message.headers = extra_headers 128 turbomail.enqueue(message) 129 msginfo = get_received_mail(self.sink) 130 msg = email.message_from_string(msginfo['mail']) 131 for name, value in extra_headers: 132 self.failUnless(msg.has_key(name)) 133 self.assertEquals(value, msg[name]) -
trunk/tests/lib/smtp_mailsink.py
r53 r67 1 1 # -*- coding: UTF-8 -*- 2 """A library which implements a SMTP mail sink (dummy SMTP server) in order 2 """A library which implements a SMTP mail sink (dummy SMTP server) in order 3 3 to test correct sending of emails.""" 4 4 … … 17 17 # You should have received a copy of the GNU Lesser General Public 18 18 # License along with this library; if not, write to the Free Software 19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 20 20 21 21 22 # This coded is based on code published on 23 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440690/ but was 22 # This coded is based on code published on 23 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440690/ but was 24 24 # heavily modified for easier usage and Python 2.3 compatibility. 25 25 # … … 27 27 # Written by Adam Feuer, Matt Branthwaite, and Troy Frever 28 28 # Published under the Python License (http://www.python.org/license). 29 29 30 30 # Example usage: 31 # 31 # 32 32 # #!/usr/bin/env python 33 33 # # -*- coding: UTF-8 -*- 34 34 # "Wait until a single message was received on localhost/port 10026" 35 # 35 # 36 36 # from smtp_mailsink import SMTPMailsink 37 37 # sink = SMTPMailsink(host='localhost', port=10026) 38 38 # sink.start() 39 39 # while not sink.has_message(): 40 # time.sleep(1)40 # time.sleep(1) 41 41 # print "received message: " + str(sink.get_messages()) 42 42 # sink.stop() … … 52 52 53 53 class SMTPMailsinkServer(smtpd.SMTPServer): 54 """This is the actual mailsink server which stores all received messages in 55 an internal queue. Do not access the queue directly. All accessor methods 56 in this object are sufficiently guarded against race conditions.""" 57 58 def __init__( self, *args, **kwargs): 59 smtpd.SMTPServer.__init__( self, *args, **kwargs ) 60 self.queued_mails = [] 61 self.lock = threading.Lock() 54 """This is the actual mailsink server which stores all received messages in 55 an internal queue. Do not access the queue directly. All accessor methods 56 in this object are sufficiently guarded against race conditions.""" 62 57 63 def has_message(self): 64 """Return True if at least one message was received successfully. The 65 access to the internal queue is synchronized so the caller will be 66 blocked until the necessary lock was aquired.""" 67 self.lock.acquire() 68 number_messages = len(self.queued_mails) 69 self.lock.release() 70 return number_messages > 0 58 def __init__( self, *args, **kwargs): 59 smtpd.SMTPServer.__init__( self, *args, **kwargs ) 60 self.queued_mails = [] 61 self.lock = threading.Lock() 71 62 72 def get_messages(self):73 """Return a copy of the internal queue with all received messages. The 74 access to the internal queue is synchronized so the caller will be 75 blocked until the necessary lock was aquired."""76 self.lock.acquire()77 messages = copy.copy(self.queued_mails)78 self.lock.release()79 return messages 63 def has_message(self): 64 """Return True if at least one message was received successfully. The 65 access to the internal queue is synchronized so the caller will be 66 blocked until the necessary lock was aquired.""" 67 self.lock.acquire() 68 number_messages = len(self.queued_mails) 69 self.lock.release() 70 return number_messages > 0 80 71 81 def pop(self, index=None): 82 """Return the index'th message in the queue (default=last) which is 83 removed from the queue afterwards. Throws IndexError if index is bigger 84 than the number of messages in the queue.""" 85 item = None 86 self.lock.acquire() 87 if index == None: 88 index = len(self.queued_mails) - 1 89 try: 90 item = self.queued_mails.pop(index) 91 except Exception: 92 print 'pop: before lock release' 93 self.lock.release() 94 raise 95 self.lock.release() 96 return item 72 def get_messages(self): 73 """Return a copy of the internal queue with all received messages. The 74 access to the internal queue is synchronized so the caller will be 75 blocked until the necessary lock was aquired.""" 76 self.lock.acquire() 77 messages = copy.copy(self.queued_mails) 78 self.lock.release() 79 return messages 80 81 def pop(self, index=None): 82 """Return the index'th message in the queue (default=last) which is 83 removed from the queue afterwards. Throws IndexError if index is bigger 84 than the number of messages in the queue.""" 85 item = None 86 self.lock.acquire() 87 if index == None: 88 index = len(self.queued_mails) - 1 89 try: 90 item = self.queued_mails.pop(index) 91 except Exception: 92 print 'pop: before lock release' 93 self.lock.release() 94 raise 95 self.lock.release() 96 return item 97 97 98 98 99 def process_message(self, peer, mailfrom, rcpttos, data):100 "Store a received message in the internal queue. For internal use only!"101 msg = {'client': peer, 'from': mailfrom, 'recipients': rcpttos, 102 'mail': data}103 self.lock.acquire()104 self.queued_mails.append(msg)105 self.lock.release()99 def process_message(self, peer, mailfrom, rcpttos, data): 100 "Store a received message in the internal queue. For internal use only!" 101 msg = {'client': peer, 'from': mailfrom, 'recipients': rcpttos, 102 'mail': data} 103 self.lock.acquire() 104 self.queued_mails.append(msg) 105 self.lock.release() 106 106 107 107 108 108 class SMTPMailsink(threading.Thread): 109 """This class is responsible for controlling the actual mailsink server 110 class.""" 109 """This class is responsible for controlling the actual mailsink server 110 class.""" 111 111 112 def __init__(self, host='localhost', port=25):113 threading.Thread.__init__(self)114 self.stop_event = threading.Event()115 self.server = SMTPMailsinkServer((host, port), None)112 def __init__(self, host='localhost', port=25): 113 threading.Thread.__init__(self) 114 self.stop_event = threading.Event() 115 self.server = SMTPMailsinkServer((host, port), None) 116 116 117 def run(self):118 "Just run in a loop until stop() is called."119 while not self.stop_event.isSet():120 try:121 asyncore.loop(timeout=0.1)122 except select.error, e:123 if e.args[0] != errno.EBADF:124 raise117 def run(self): 118 "Just run in a loop until stop() is called." 119 while not self.stop_event.isSet(): 120 try: 121 asyncore.loop(timeout=0.1) 122 except select.error, e: 123 if e.args[0] != errno.EBADF: 124 raise 125 125 126 def stop(self, timeout_seconds=5.0): 127 """Stop the mailsink and shut down this thread. timeout_seconds 128 specifies how long the caller should wait for the mailsink server to 129 close down (default: 5 seconds). If the server did not stop in time, a 130 warning message is printed.""" 131 self.stop_event.set() 132 self.server.close() 133 threading.Thread.join(self, timeout=timeout_seconds) 134 if self.isAlive(): 135 print "WARNING: Thread still alive. Timeout while waiting for " + \ 136 "termination!" 137 138 def has_message(self): 139 "Return True if at least one message was received successfully." 140 return self.server.has_message() 141 142 def get_messages(self): 143 "Return a copy of the internal queue with all received messages." 144 return self.server.get_messages() 126 def stop(self, timeout_seconds=5.0): 127 """Stop the mailsink and shut down this thread. timeout_seconds 128 specifies how long the caller should wait for the mailsink server to 129 close down (default: 5 seconds). If the server did not stop in time, a 130 warning message is printed.""" 131 self.stop_event.set() 132 self.server.close() 133 threading.Thread.join(self, timeout=timeout_seconds) 134 if self.isAlive(): 135 print "WARNING: Thread still alive. Timeout while waiting for " + \ 136 "termination!" 145 137 146 def pop(self, index=None): 147 """Return the index'th message in the queue (default=last) which is 148 removed from the queue afterwards. Throws IndexError if index is bigger 149 than the number of messages in the queue.""" 150 return self.server.pop(index) 138 def has_message(self): 139 "Return True if at least one message was received successfully." 140 return self.server.has_message() 141 142 def get_messages(self): 143 "Return a copy of the internal queue with all received messages." 144 return self.server.get_messages() 145 146 def pop(self, index=None): 147 """Return the index'th message in the queue (default=last) which is 148 removed from the queue afterwards. Throws IndexError if index is bigger 149 than the number of messages in the queue.""" 150 return self.server.pop(index) -
trunk/tests/lib/utils.py
r47 r67 5 5 # This code is placed under the MIT license: 6 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the "Software"), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the "Software"), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 12 # furnished to do so, subject to the following conditions: 13 13 # 14 # The above copyright notice and this permission notice shall be included in 14 # The above copyright notice and this permission notice shall be included in 15 15 # all copies or substantial portions of the Software. 16 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 23 # SOFTWARE. 24 24 … … 29 29 30 30 class TimeoutException(Exception): 31 "A timeout occured!"32 pass31 "A timeout occured!" 32 pass 33 33 34 34 35 35 def get_received_mail(sink): 36 """Return the first mail received (which is removed from the TurboMail37 queue) in the following format {'client': ...., 'from': ..., 38 'recipients': ..., 'mail': ...}. Blocks the caller until a message was 39 received but no longer than 10 seconds (in this case, a TimeoutException40 is thrown)."""41 for i in range(10 * 10):42 if not sink.has_message():43 time.sleep(0.1)44 else:45 break46 if not sink.has_message():47 raise TimeoutException()48 return sink.pop(index=0)36 """Return the first mail received (which is removed from the TurboMail 37 queue) in the following format {'client': ...., 'from': ..., 38 'recipients': ..., 'mail': ...}. Blocks the caller until a message was 39 received but no longer than 10 seconds (in this case, a TimeoutException 40 is thrown)."""
