[tor-commits] [bridgedb/main] Attach QR codes of bridges on email distributor
meskio at torproject.org
meskio at torproject.org
Thu May 13 13:53:53 UTC 2021
commit 10306676c94a9abd4307870074407cb2fe94d63e
Author: meskio <meskio at torproject.org>
Date: Mon May 10 11:50:06 2021 +0200
Attach QR codes of bridges on email distributor
Refactor the code to use email.message.EmailMessage instead of the custom
EmailResponse implementation.
Fixes #40008
---
bridgedb/distributors/email/autoresponder.py | 260 +++++----------------------
bridgedb/test/test_email_autoresponder.py | 171 +++---------------
2 files changed, 78 insertions(+), 353 deletions(-)
diff --git a/bridgedb/distributors/email/autoresponder.py b/bridgedb/distributors/email/autoresponder.py
index 94a8f75..1632258 100644
--- a/bridgedb/distributors/email/autoresponder.py
+++ b/bridgedb/distributors/email/autoresponder.py
@@ -21,7 +21,7 @@ bridgedb.distributors.email.autoresponder
Functionality for autoresponding to incoming emails.
-.. inheritance-diagram:: EmailResponse SMTPAutoresponder
+.. inheritance-diagram:: SMTPAutoresponder
:parts: 1
::
@@ -31,7 +31,6 @@ Functionality for autoresponding to incoming emails.
| | how to respond.
| |_ generateResponse - Create an email response.
|
- |_ EmailResponse - Holds information for generating a response to a request.
|_ SMTPAutoresponder - An SMTP autoresponder for incoming mail.
..
"""
@@ -39,11 +38,12 @@ Functionality for autoresponding to incoming emails.
from __future__ import unicode_literals
from __future__ import print_function
-import email
import io
import logging
import time
+from email.utils import parseaddr
+from email.message import EmailMessage
from twisted.internet import defer
from twisted.internet import reactor
from twisted.mail import smtp
@@ -59,6 +59,7 @@ from bridgedb.distributors.email.distributor import TooSoonEmail
from bridgedb.distributors.email.distributor import IgnoreEmail
from bridgedb.parse import addr
from bridgedb.parse.addr import canonicalizeEmailDomain
+from bridgedb.qrcodes import generateQR
from bridgedb.util import levenshteinDistance
from bridgedb import translations
@@ -85,11 +86,12 @@ def createResponseBody(lines, context, client, lang='en'):
email to `bridges+fa at torproject.org
<mailto:bridges+fa at torproject.org>`__, the client should receive a
response in Farsi.
- :rtype: str
+ :rtype: (str, bytes)
:returns: ``None`` if we shouldn't respond to the client (i.e., if they
have already received a rate-limiting warning email). Otherwise,
returns a string containing the (optionally translated) body for the
- email response which we should send out.
+ email response which we should send out and the qrcode image of the
+ bridges if we provide bridges.
"""
translator = translations.installTranslations(lang)
bridges = None
@@ -102,28 +104,28 @@ def createResponseBody(lines, context, client, lang='en'):
bridges = context.distributor.getBridges(bridgeRequest, interval)
except TooSoonEmail as error:
logging.info("Got a mail too frequently: %s." % error)
- return templates.buildSpamWarning(translator, client)
+ return templates.buildSpamWarning(translator, client), None
except (IgnoreEmail, addr.BadEmail) as error:
logging.info(error)
# Don't generate a response if their email address is unparsable or
# invalid, or if we've already warned them about rate-limiting:
- return None
+ return None, None
else:
answer = "(no bridges currently available)\r\n"
+ qrcode = None
if bridges:
transport = bridgeRequest.justOnePTType()
- answer = "".join(" %s\r\n" % b.getBridgeLine(
- bridgeRequest, context.includeFingerprints) for b in bridges)
+ bridgeLines = [b.getBridgeLine(bridgeRequest, context.includeFingerprints) for b in bridges]
+ answer = "".join(" %s\r\n" % line for line in bridgeLines)
+ qrcode = generateQR(bridgeLines)
internalMetrix.recordHandoutsPerBridge(bridgeRequest, bridges)
else:
internalMetrix.recordEmptyEmailResponse()
- return templates.buildAnswerMessage(translator, client, answer)
+ return templates.buildAnswerMessage(translator, client, answer), qrcode
def generateResponse(fromAddress, client, body, subject=None,
- messageID=None):
- """Create an :class:`EmailResponse`, which acts like an
- :class:`io.StringIO` instance, by creating and writing all headers and the
- email body into the file-like :attr:`EmailResponse.mailfile`.
+ messageID=None, qrcode=None):
+ """Create an :class:`email.message.EmailMessage`
:param str fromAddress: The :rfc:`2821` email address which should be in
the ``'From:'`` header.
@@ -135,206 +137,43 @@ def generateResponse(fromAddress, client, body, subject=None,
:type messageID: ``None`` or :any:`str`
:param messageID: The :rfc:`2822` specifier for the ``'Message-ID:'``
header, if including one is desirable.
- :returns: An :class:`EmailResponse` which contains the entire email. To
- obtain the contents of the email, including all headers, simply use
- :meth:`EmailResponse.readContents`.
+ :returns: An :class:`email.message.EmailMessage` which contains the entire
+ email. To obtain the contents of the email, including all headers,
+ simply use :meth:`EmailMessage.as_string`.
"""
- response = EmailResponse()
- response.to = client
- response.writeHeaders(fromAddress.encode('utf-8'), str(client), subject,
- inReplyTo=messageID)
- response.writeBody(body.encode('utf-8'))
+ response = EmailMessage()
+ response["From"] = fromAddress
+ response["To"] = str(client)
+
+ if not subject:
+ response["Subject"] = '[no subject]'
+ else:
+ response["Subject"] = subject
+ if messageID:
+ response.add_header("In-Reply-To", messageID)
+
+ response.add_header("Date", smtp.rfc822date().decode("utf-8"))
+ response.set_content(body)
+
+ if qrcode:
+ response.add_attachment(qrcode, maintype="image", subtype="jpeg", filename="qrcode.jpg")
# Only log the email text (including all headers) if SAFE_LOGGING is
# disabled:
if not safelog.safe_logging:
- contents = response.readContents()
- logging.debug("Email contents:\n%s" % str(contents))
+ logging.debug("Email contents:\n%s" % response.as_string())
else:
logging.debug("Email text for %r created." % str(client))
- response.rewind()
return response
-class EmailResponse(object):
- """Holds information for generating a response email for a request.
-
- .. todo:: At some point, we may want to change this class to optionally
- handle creating Multipart MIME encoding messages, so that we can
- include attachments.
-
- :var str delimiter: Delimiter between lines written to the
- :data:`mailfile`.
- :var bool closed: ``True`` if :meth:`close` has been called.
- :vartype to: :api:`twisted.mail.smtp.Address`
- :var to: The client's email address, to which this response should be sent.
- """
-
- def __init__(self):
- """Create a response to an email we have recieved.
-
- This class deals with correctly formatting text for the response email
- headers and the response body into an instance of :data:`mailfile`.
- """
- self.mailfile = io.StringIO()
- self.delimiter = '\n'
- self.closed = False
- self.to = None
-
- def close(self):
- """Close our :data:`mailfile` and set :data:`closed` to ``True``."""
- logging.debug("Closing %s.mailfile..." % (self.__class__.__name__))
- self.mailfile.close()
- self.closed = True
-
- def read(self, size=None):
- """Read, at most, **size** bytes from our :data:`mailfile`.
-
- .. note:: This method is required by Twisted's SMTP system.
-
- :param int size: The number of bytes to read. Defaults to ``None``,
- which reads until EOF.
- :rtype: str
- :returns: The bytes read from the :data:`mailfile`.
- """
- contents = ''
- logging.debug("Reading%s from %s.mailfile..."
- % ((' {0} bytes'.format(size) if size else ''),
- self.__class__.__name__))
- try:
- if size is not None:
- contents = self.mailfile.read(int(size)).encode('utf-8')
- else:
- contents = self.mailfile.read().encode('utf-8')
- except Exception as error: # pragma: no cover
- logging.exception(error)
-
- return contents
-
- def readContents(self):
- """Read the all the contents written thus far to the :data:`mailfile`,
- and then :meth:`seek` to return to the original pointer position we
- were at before this method was called.
-
- :rtype: str
- :returns: The entire contents of the :data:`mailfile`.
- """
- pointer = self.mailfile.tell()
- self.mailfile.seek(0)
- contents = self.mailfile.read()
- self.mailfile.seek(pointer)
- return contents
-
- def rewind(self):
- """Rewind to the very beginning of the :data:`mailfile`."""
- logging.debug("Rewinding %s.mailfile..." % self.__class__.__name__)
- self.mailfile.seek(0)
-
- def write(self, line):
- """Write the **line** to the :data:`mailfile`.
-
- Any **line** written to me will have :data:`delimiter` appended to it
- beforehand.
-
- :param str line: Something to append into the :data:`mailfile`.
- """
-
- line = line.decode('utf-8') if isinstance(line, bytes) else line
-
- if line.find('\r\n') != -1:
- # If **line** contains newlines, send it to :meth:`writelines` to
- # break it up so that we can replace them:
- logging.debug("Found newlines in %r. Calling writelines()." % line)
- self.writelines(line)
- else:
- line += self.delimiter
- self.mailfile.write(line)
- self.mailfile.flush()
-
- def writelines(self, lines):
- """Calls :meth:`write` for each line in **lines**.
-
- Line endings of ``'\\r\\n'`` will be replaced with :data:`delimiter`
- (i.e. ``'\\n'``). See :api:`twisted.mail.smtp.SMTPClient.getMailData`
- for the reason.
-
- :type lines: :any:`str` or :any:`list`
- :param lines: The lines to write to the :attr:`mailfile`.
- """
- if isinstance(lines, (str, bytes)):
- lines = lines.decode('utf-8') if isinstance(lines, bytes) else lines
- lines = lines.replace('\r\n', '\n')
- for ln in lines.split('\n'):
- self.write(ln)
- elif isinstance(lines, (list, tuple,)):
- for ln in lines:
- self.write(ln)
-
- def writeHeaders(self, fromAddress, toAddress, subject=None,
- inReplyTo=None, includeMessageID=True,
- contentType='text/plain; charset="utf-8"', **kwargs):
- """Write all headers into the response email.
-
- :param str fromAddress: The email address for the ``'From:'`` header.
- :param str toAddress: The email address for the ``'To:'`` header.
- :type subject: ``None`` or :any:`str`
- :param subject: The ``'Subject:'`` header.
- :type inReplyTo: ``None`` or :any:`str`
- :param inReplyTo: If set, an ``'In-Reply-To:'`` header will be
- generated. This should be set to the ``'Message-ID:'`` header from
- the client's original request email.
- :param bool includeMessageID: If ``True``, generate and include a
- ``'Message-ID:'`` header for the response.
- :param str contentType: The ``'Content-Type:'`` header.
- :kwargs: If given, the key will become the name of the header, and the
- value will become the contents of that header.
- """
-
- fromAddress = fromAddress.decode('utf-8') if isinstance(fromAddress, bytes) else fromAddress
- toAddress = toAddress.decode('utf-8') if isinstance(toAddress, bytes) else toAddress
-
- self.write("From: %s" % fromAddress)
- self.write("To: %s" % toAddress)
- if includeMessageID:
- self.write("Message-ID: %s" % smtp.messageid())
- if inReplyTo:
- self.write("In-Reply-To: %s" % inReplyTo)
- self.write("Content-Type: %s" % contentType)
- self.write("Date: %s" % smtp.rfc822date().decode('utf-8'))
-
- if not subject:
- subject = '[no subject]'
- if not subject.lower().startswith('re'):
- subject = "Re: " + subject
- self.write("Subject: %s" % subject)
-
- if kwargs:
- for headerName, headerValue in kwargs.items():
- headerName = headerName.capitalize()
- headerName = headerName.replace(' ', '-')
- headerName = headerName.replace('_', '-')
- header = "%s: %s" % (headerName, headerValue)
- self.write(header)
-
- # The first blank line designates that the headers have ended:
- self.write(self.delimiter)
-
- def writeBody(self, body):
- """Write the response body into the :attr:`mailfile`.
-
- :param str body: The body of the response email.
- """
- logging.info("Writing email body...")
- self.writelines(body)
-
-
class SMTPAutoresponder(smtp.SMTPClient):
"""An :api:`twisted.mail.smtp.SMTPClient` for responding to incoming mail.
The main worker in this class is the :meth:`reply` method, which functions
to dissect an incoming email from an incoming :class:`SMTPMessage` and
- create a :class:`EmailResponse` email message in reply to it, and then,
+ create a :class:`EmailMessage` email message in reply to it, and then,
finally, send it out.
:vartype log: :api:`twisted.python.util.LineLog`
@@ -370,8 +209,8 @@ class SMTPAutoresponder(smtp.SMTPClient):
This method must return a file-like object containing the data of the
message to be sent. Lines in the file should be delimited by ``\\n``.
- :rtype: ``None`` or :class:`EmailResponse`
- :returns: An ``EmailResponse``, if we have a response to send in reply
+ :rtype: ``None`` or :class:`EmailMessage`
+ :returns: An ``EmailMessage``, if we have a response to send in reply
to the incoming email, otherwise, returns ``None``.
"""
clients = self.getMailTo()
@@ -391,9 +230,9 @@ class SMTPAutoresponder(smtp.SMTPClient):
lang = translations.getLocaleFromPlusAddr(recipient)
logging.info("Client requested email translation: %s" % lang)
- body = createResponseBody(self.incoming.lines,
- self.incoming.context,
- client, lang)
+ body, qrcode = createResponseBody(self.incoming.lines,
+ self.incoming.context,
+ client, lang)
# The string EMAIL_MISC_TEXT[1] shows up in an email if BridgeDB
# responds with bridges. Everything else we count as an invalid
@@ -408,8 +247,7 @@ class SMTPAutoresponder(smtp.SMTPClient):
messageID = self.incoming.message.get("Message-ID", None)
subject = self.incoming.message.get("Subject", None)
- response = generateResponse(recipient, client,
- body, subject, messageID)
+ response = generateResponse(recipient, client, body, subject, messageID, qrcode)
return response
def getMailTo(self):
@@ -427,13 +265,13 @@ class SMTPAutoresponder(smtp.SMTPClient):
"""
clients = []
addrHeader = None
- try: fromAddr = email.utils.parseaddr(self.incoming.message.get("From"))[1]
+ try: fromAddr = parseaddr(self.incoming.message.get("From"))[1]
except (IndexError, TypeError, AttributeError): pass
else: addrHeader = fromAddr
if not addrHeader:
logging.warn("No From header on incoming mail.")
- try: senderHeader = email.utils.parseaddr(self.incoming.message.get("Sender"))[1]
+ try: senderHeader = parseaddr(self.incoming.message.get("Sender"))[1]
except (IndexError, TypeError, AttributeError): pass
else: addrHeader = senderHeader
if not addrHeader:
@@ -675,17 +513,17 @@ class SMTPAutoresponder(smtp.SMTPClient):
:type client: :api:`twisted.mail.smtp.Address`
:param client: The email address of the client.
- :param response: A :class:`EmailResponse`.
+ :param response: A :class:`EmailMessage`.
:param int retries: Try resending this many times. (default: ``0``)
:param int timeout: Timeout after this many seconds. (default: ``30``)
:rtype: :api:`Deferred <twisted.internet.defer.Deferred>`
:returns: Our :data:`deferred`.
"""
- logging.info("Sending reply to %s ..." % str(response.to))
+ logging.info("Sending reply to %s ..." % str(response["To"]))
factory = smtp.SMTPSenderFactory(self.incoming.context.smtpFromAddr,
- str(response.to),
- response,
+ response["To"],
+ io.BytesIO(response.as_bytes()),
self.deferred,
retries=retries,
timeout=timeout)
diff --git a/bridgedb/test/test_email_autoresponder.py b/bridgedb/test/test_email_autoresponder.py
index 9b49c67..566d837 100644
--- a/bridgedb/test/test_email_autoresponder.py
+++ b/bridgedb/test/test_email_autoresponder.py
@@ -59,16 +59,18 @@ class CreateResponseBodyTests(unittest.TestCase):
bridges."""
lines = self._getIncomingLines("testing at localhost")
lines[4] = "transport obfs3"
- ret = autoresponder.createResponseBody(lines, self.ctx, self.toAddress)
+ ret, qrcode = autoresponder.createResponseBody(lines, self.ctx, self.toAddress)
self.assertSubstring("Here are your bridges:", ret)
+ self.assertIsNotNone(qrcode)
def test_createResponseBody_bridges_obfs3(self):
"""A request for 'get transport obfs3' should receive a response."""
lines = self._getIncomingLines("testing at localhost")
lines[4] = "get transport obfs3"
- ret = autoresponder.createResponseBody(lines, self.ctx, self.toAddress)
+ ret, qrcode = autoresponder.createResponseBody(lines, self.ctx, self.toAddress)
self.assertSubstring("Here are your bridges", ret)
self.assertSubstring("obfs3", ret)
+ self.assertIsInstance(qrcode, bytes)
def test_createResponseBody_bridges_obfsobfswebz(self):
"""We should only pay attention to the *last* in a crazy request."""
@@ -76,9 +78,10 @@ class CreateResponseBodyTests(unittest.TestCase):
lines[4] = "get unblocked webz"
lines.append("get transport obfs2")
lines.append("get transport obfs3")
- ret = autoresponder.createResponseBody(lines, self.ctx, self.toAddress)
+ ret, qrcode = autoresponder.createResponseBody(lines, self.ctx, self.toAddress)
self.assertSubstring("Here are your bridges", ret)
self.assertSubstring("obfs3", ret)
+ self.assertIsInstance(qrcode, bytes)
def test_createResponseBody_bridges_obfsobfswebzipv6(self):
"""We should *still* only pay attention to the *last* request."""
@@ -87,9 +90,10 @@ class CreateResponseBodyTests(unittest.TestCase):
lines.append("get unblocked webz")
lines.append("get ipv6")
lines.append("get transport obfs2")
- ret = autoresponder.createResponseBody(lines, self.ctx, self.toAddress)
+ ret, qrcode = autoresponder.createResponseBody(lines, self.ctx, self.toAddress)
self.assertSubstring("Here are your bridges", ret)
self.assertSubstring("obfs2", ret)
+ self.assertIsInstance(qrcode, bytes)
def test_createResponseBody_two_requests_TooSoonEmail(self):
"""The same client making two requests in a row should receive a
@@ -100,10 +104,12 @@ class CreateResponseBodyTests(unittest.TestCase):
ctx = _createMailServerContext(self.config, dist)
lines = self._getIncomingLines("testing at localhost")
- first = autoresponder.createResponseBody(lines, ctx, self.toAddress)
+ first, qrcode = autoresponder.createResponseBody(lines, ctx, self.toAddress)
self.assertSubstring("Here are your bridges", first)
- second = autoresponder.createResponseBody(lines, ctx, self.toAddress)
+ self.assertIsInstance(qrcode, bytes)
+ second, qrcode = autoresponder.createResponseBody(lines, ctx, self.toAddress)
self.assertSubstring("Please slow down", second)
+ self.assertIsNone(qrcode)
def test_createResponseBody_three_requests_TooSoonEmail(self):
"""Alice making a request, next Bob making a request, and then Alice again,
@@ -115,18 +121,21 @@ class CreateResponseBodyTests(unittest.TestCase):
ctx = _createMailServerContext(self.config, dist)
aliceLines = self._getIncomingLines("alice at localhost")
- aliceFirst = autoresponder.createResponseBody(aliceLines, ctx,
- self.toAddress)
+ aliceFirst, qrcode = autoresponder.createResponseBody(aliceLines, ctx,
+ self.toAddress)
self.assertSubstring("Here are your bridges", aliceFirst)
+ self.assertIsInstance(qrcode, bytes)
bobLines = self._getIncomingLines("bob at localhost")
- bobFirst = autoresponder.createResponseBody(bobLines, ctx,
- self.toAddress)
+ bobFirst, qrcode = autoresponder.createResponseBody(bobLines, ctx,
+ self.toAddress)
self.assertSubstring("Here are your bridges", bobFirst)
+ self.assertIsInstance(qrcode, bytes)
- aliceSecond = autoresponder.createResponseBody(aliceLines, ctx,
- self.toAddress)
+ aliceSecond, qrcode = autoresponder.createResponseBody(aliceLines, ctx,
+ self.toAddress)
self.assertSubstring("Please slow down", aliceSecond)
+ self.assertIsNone(qrcode)
def test_createResponseBody_three_requests_IgnoreEmail(self):
"""The same client making three requests in a row should receive a
@@ -138,140 +147,18 @@ class CreateResponseBodyTests(unittest.TestCase):
ctx = _createMailServerContext(self.config, dist)
lines = self._getIncomingLines("testing at localhost")
- first = autoresponder.createResponseBody(lines, ctx, self.toAddress)
+ first, qrcode = autoresponder.createResponseBody(lines, ctx, self.toAddress)
self.assertSubstring("Here are your bridges", first)
- second = autoresponder.createResponseBody(lines, ctx, self.toAddress)
+ self.assertIsInstance(qrcode, bytes)
+ second, qrcode = autoresponder.createResponseBody(lines, ctx, self.toAddress)
self.assertSubstring("Please slow down", second)
- third = autoresponder.createResponseBody(lines, ctx, self.toAddress)
+ self.assertIsNone(qrcode)
+ third, qrcode = autoresponder.createResponseBody(lines, ctx, self.toAddress)
self.assertIsNone(third)
- fourth = autoresponder.createResponseBody(lines, ctx, self.toAddress)
+ self.assertIsNone(qrcode)
+ fourth, qrcode = autoresponder.createResponseBody(lines, ctx, self.toAddress)
self.assertIsNone(fourth)
-
-
-class EmailResponseTests(unittest.TestCase):
- """Tests for ``generateResponse()`` and ``EmailResponse``."""
-
- def setUp(self):
- self.fromAddr = "bridges at torproject.org"
- self.clientAddr = "user at example.com"
- self.body = """\
-People think that time is strictly linear, but, in reality, it's actually just
-a ball of timey-wimey, wibbly-warbly... stuff."""
-
- def tearDown(self):
- autoresponder.safelog.safe_logging = True
-
- def test_EmailResponse_generateResponse(self):
- response = autoresponder.generateResponse(self.fromAddr,
- self.clientAddr,
- self.body)
- self.assertIsInstance(response, autoresponder.EmailResponse)
-
- def test_EmailResponse_generateResponse_noSafelog(self):
- autoresponder.safelog.safe_logging = False
- response = autoresponder.generateResponse(self.fromAddr,
- self.clientAddr,
- self.body)
- self.assertIsInstance(response, autoresponder.EmailResponse)
-
- def test_EmailResponse_generateResponse_mailfile(self):
- response = autoresponder.generateResponse(self.fromAddr,
- self.clientAddr,
- self.body)
- self.assertIsInstance(response.mailfile, (io.BytesIO, io.StringIO))
-
- def test_EmailResponse_generateResponse_withInReplyTo(self):
- response = autoresponder.generateResponse(self.fromAddr,
- self.clientAddr,
- self.body,
- messageID="NSA")
- contents = str(response.readContents()).replace('\x00', '')
- self.assertIsInstance(response.mailfile, (io.BytesIO, io.StringIO))
- self.assertSubstring("In-Reply-To: NSA", contents)
-
- def test_EmailResponse_generateResponse_readContents(self):
- response = autoresponder.generateResponse(self.fromAddr,
- self.clientAddr,
- self.body)
- contents = str(response.readContents()).replace('\x00', '')
- self.assertSubstring('timey-wimey, wibbly-warbly... stuff.', contents)
-
- def test_EmailResponse_additionalHeaders(self):
- response = autoresponder.EmailResponse()
- response.writeHeaders(self.fromAddr, self.clientAddr,
- subject="Re: echelon", inReplyTo="NSA",
- X_been_there="They were so 2004")
- contents = str(response.readContents()).replace('\x00', '')
- self.assertIsInstance(response.mailfile, (io.BytesIO, io.StringIO))
- self.assertSubstring("In-Reply-To: NSA", contents)
- self.assertSubstring("X-been-there: They were so 2004", contents)
-
- def test_EmailResponse_close(self):
- """Calling EmailResponse.close() should close the ``mailfile`` and set
- ``closed=True``.
- """
- response = autoresponder.EmailResponse()
- self.assertEqual(response.closed, False)
- response.close()
- self.assertEqual(response.closed, True)
- self.assertRaises(ValueError, response.write, self.body)
-
- def test_EmailResponse_read(self):
- """Calling EmailResponse.read() should read bytes from the file."""
- response = autoresponder.EmailResponse()
- response.write(self.body)
- response.rewind()
- contents = response.read().replace(b'\x00', b'').decode('utf-8')
- # The newlines in the email body should have been replaced with
- # ``EmailResponse.delimiter``.
- delimited = self.body.replace('\n', response.delimiter) \
- + response.delimiter
- self.assertEqual(delimited, contents)
-
- def test_EmailResponse_read_three_bytes(self):
- """EmailResponse.read(3) should read three bytes from the file."""
- response = autoresponder.EmailResponse()
- response.write(self.body)
- response.rewind()
- contents = response.read(3).replace(b'\x00', b'').decode('utf-8')
- self.assertEqual(contents, self.body[:3])
-
- def test_EmailResponse_write(self):
- """Calling EmailResponse.write() should write to the mailfile."""
- response = autoresponder.EmailResponse()
- response.write(self.body)
- contents = str(response.readContents()).replace('\x00', '')
- # The newlines in the email body should have been replaced with
- # ``EmailResponse.delimiter``.
- delimited = self.body.replace('\n', response.delimiter) \
- + response.delimiter
- self.assertEqual(delimited, contents)
-
- def test_EmailResponse_write_withRetNewlines(self):
- """Calling EmailResponse.write() with '\r\n' in the lines should call
- writelines(), which splits up the lines and then calls write() again.
- """
- response = autoresponder.EmailResponse()
- response.write(self.body.replace('\n', '\r\n'))
- contents = str(response.readContents()).replace('\x00', '')
- # The newlines in the email body should have been replaced with
- # ``EmailResponse.delimiter``.
- delimited = self.body.replace('\n', response.delimiter) \
- + response.delimiter
- self.assertEqual(delimited, contents)
-
- def test_EmailResponse_writelines_list(self):
- """Calling EmailResponse.writelines() with a list should write the
- concatenated contents of the list into the mailfile.
- """
- response = autoresponder.EmailResponse()
- response.writelines(self.body.split('\n'))
- contents = str(response.readContents()).replace('\x00', '')
- # The newlines in the email body should have been replaced with
- # ``EmailResponse.delimiter``.
- delimited = self.body.replace('\n', response.delimiter) \
- + response.delimiter
- self.assertEqual(delimited, contents)
+ self.assertIsNone(qrcode)
class SMTPAutoresponderTests(unittest.TestCase):
More information about the tor-commits
mailing list