%PDF- %PDF-
Direktori : /lib/python3/dist-packages/twisted/mail/test/ |
Current File : //lib/python3/dist-packages/twisted/mail/test/test_imap.py |
# -*- test-case-name: twisted.mail.test.test_imap -*- # Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ Test case for twisted.mail.imap4 """ from __future__ import annotations import base64 import codecs import functools import locale import os import uuid from collections import OrderedDict from io import BytesIO from itertools import chain from typing import Optional, Type from unittest import skipIf from zope.interface import implementer from zope.interface.verify import verifyClass, verifyObject from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse from twisted.cred.credentials import ( CramMD5Credentials, IUsernameHashedPassword, IUsernamePassword, ) from twisted.cred.error import UnauthorizedLogin from twisted.cred.portal import IRealm, Portal from twisted.internet import defer, error, interfaces, reactor from twisted.internet.defer import Deferred from twisted.internet.task import Clock from twisted.internet.testing import StringTransport, StringTransportWithDisconnection from twisted.mail import imap4 from twisted.mail.imap4 import MessageSet from twisted.mail.interfaces import ( IChallengeResponse, IClientAuthentication, ICloseableMailboxIMAP, ) from twisted.protocols import loopback from twisted.python import failure, log, util from twisted.python.compat import iterbytes, nativeString, networkString from twisted.trial.unittest import SynchronousTestCase, TestCase try: from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext except ImportError: ClientTLSContext = None # type: ignore[assignment,misc] ServerTLSContext = None # type: ignore[assignment,misc] def strip(f): return lambda result, f=f: f() class IMAP4UTF7Tests(TestCase): tests = [ ["Hello world", b"Hello world"], ["Hello & world", b"Hello &- world"], ["Hello\xffworld", b"Hello&AP8-world"], ["\xff\xfe\xfd\xfc", b"&AP8A,gD9APw-"], [ "~peter/mail/\u65e5\u672c\u8a9e/\u53f0\u5317", b"~peter/mail/&ZeVnLIqe-/&U,BTFw-", ], # example from RFC 2060 ] def test_encodeWithErrors(self): """ Specifying an error policy to C{unicode.encode} with the I{imap4-utf-7} codec should produce the same result as not specifying the error policy. """ text = "Hello world" self.assertEqual( text.encode("imap4-utf-7", "strict"), text.encode("imap4-utf-7") ) def test_decodeWithErrors(self): """ Similar to L{test_encodeWithErrors}, but for C{bytes.decode}. """ bytes = b"Hello world" self.assertEqual( bytes.decode("imap4-utf-7", "strict"), bytes.decode("imap4-utf-7") ) def test_encodeAmpersand(self): """ Unicode strings that contain an ampersand (C{&}) can be encoded to bytes with the I{imap4-utf-7} codec. """ text = "&Hello&\N{VULGAR FRACTION ONE HALF}&" self.assertEqual( text.encode("imap4-utf-7"), b"&-Hello&-&AL0-&-", ) def test_decodeWithoutFinalASCIIShift(self): """ An I{imap4-utf-7} encoded string that does not shift back to ASCII (i.e., it lacks a final C{-}) can be decoded. """ self.assertEqual( b"&AL0".decode("imap4-utf-7"), "\N{VULGAR FRACTION ONE HALF}", ) def test_getreader(self): """ C{codecs.getreader('imap4-utf-7')} returns the I{imap4-utf-7} stream reader class. """ reader = codecs.getreader("imap4-utf-7")(BytesIO(b"Hello&AP8-world")) self.assertEqual(reader.read(), "Hello\xffworld") def test_getwriter(self): """ C{codecs.getwriter('imap4-utf-7')} returns the I{imap4-utf-7} stream writer class. """ output = BytesIO() writer = codecs.getwriter("imap4-utf-7")(output) writer.write("Hello\xffworld") self.assertEqual(output.getvalue(), b"Hello&AP8-world") def test_encode(self): """ The I{imap4-utf-7} can be used to encode a unicode string into a byte string according to the IMAP4 modified UTF-7 encoding rules. """ for input, output in self.tests: self.assertEqual(input.encode("imap4-utf-7"), output) def test_decode(self): """ The I{imap4-utf-7} can be used to decode a byte string into a unicode string according to the IMAP4 modified UTF-7 encoding rules. """ for input, output in self.tests: self.assertEqual(input, output.decode("imap4-utf-7")) def test_printableSingletons(self): """ The IMAP4 modified UTF-7 implementation encodes all printable characters which are in ASCII using the corresponding ASCII byte. """ # All printables represent themselves for o in chain(range(0x20, 0x26), range(0x27, 0x7F)): charbyte = chr(o).encode() self.assertEqual(charbyte, chr(o).encode("imap4-utf-7")) self.assertEqual(chr(o), charbyte.decode("imap4-utf-7")) self.assertEqual("&".encode("imap4-utf-7"), b"&-") self.assertEqual(b"&-".decode("imap4-utf-7"), "&") class BufferingConsumer: def __init__(self): self.buffer = [] def write(self, bytes): self.buffer.append(bytes) if self.consumer: self.consumer.resumeProducing() def registerProducer(self, consumer, streaming): self.consumer = consumer self.consumer.resumeProducing() def unregisterProducer(self): self.consumer = None class MessageProducerTests(SynchronousTestCase): def testSinglePart(self): body = b"This is body text. Rar." headers = OrderedDict() headers["from"] = "sender@host" headers["to"] = "recipient@domain" headers["subject"] = "booga booga boo" headers["content-type"] = "text/plain" msg = FakeyMessage(headers, (), None, body, 123, None) c = BufferingConsumer() p = imap4.MessageProducer(msg) d = p.beginProducing(c) def cbProduced(result): self.assertIdentical(result, p) self.assertEqual( b"".join(c.buffer), b"{119}\r\n" b"From: sender@host\r\n" b"To: recipient@domain\r\n" b"Subject: booga booga boo\r\n" b"Content-Type: text/plain\r\n" b"\r\n" + body, ) return d.addCallback(cbProduced) def testSingleMultiPart(self): outerBody = b"" innerBody = b"Contained body message text. Squarge." headers = OrderedDict() headers["from"] = "sender@host" headers["to"] = "recipient@domain" headers["subject"] = "booga booga boo" headers["content-type"] = 'multipart/alternative; boundary="xyz"' innerHeaders = OrderedDict() innerHeaders["subject"] = "this is subject text" innerHeaders["content-type"] = "text/plain" msg = FakeyMessage( headers, (), None, outerBody, 123, [FakeyMessage(innerHeaders, (), None, innerBody, None, None)], ) c = BufferingConsumer() p = imap4.MessageProducer(msg) d = p.beginProducing(c) def cbProduced(result): self.failUnlessIdentical(result, p) self.assertEqual( b"".join(c.buffer), b"{239}\r\n" b"From: sender@host\r\n" b"To: recipient@domain\r\n" b"Subject: booga booga boo\r\n" b'Content-Type: multipart/alternative; boundary="xyz"\r\n' b"\r\n" b"\r\n" b"--xyz\r\n" b"Subject: this is subject text\r\n" b"Content-Type: text/plain\r\n" b"\r\n" + innerBody + b"\r\n--xyz--\r\n", ) return d.addCallback(cbProduced) def testMultipleMultiPart(self): outerBody = b"" innerBody1 = b"Contained body message text. Squarge." innerBody2 = b"Secondary <i>message</i> text of squarge body." headers = OrderedDict() headers["from"] = "sender@host" headers["to"] = "recipient@domain" headers["subject"] = "booga booga boo" headers["content-type"] = 'multipart/alternative; boundary="xyz"' innerHeaders = OrderedDict() innerHeaders["subject"] = "this is subject text" innerHeaders["content-type"] = "text/plain" innerHeaders2 = OrderedDict() innerHeaders2["subject"] = "<b>this is subject</b>" innerHeaders2["content-type"] = "text/html" msg = FakeyMessage( headers, (), None, outerBody, 123, [ FakeyMessage(innerHeaders, (), None, innerBody1, None, None), FakeyMessage(innerHeaders2, (), None, innerBody2, None, None), ], ) c = BufferingConsumer() p = imap4.MessageProducer(msg) d = p.beginProducing(c) def cbProduced(result): self.failUnlessIdentical(result, p) self.assertEqual( b"".join(c.buffer), b"{354}\r\n" b"From: sender@host\r\n" b"To: recipient@domain\r\n" b"Subject: booga booga boo\r\n" b'Content-Type: multipart/alternative; boundary="xyz"\r\n' b"\r\n" b"\r\n" b"--xyz\r\n" b"Subject: this is subject text\r\n" b"Content-Type: text/plain\r\n" b"\r\n" + innerBody1 + b"\r\n--xyz\r\n" b"Subject: <b>this is subject</b>\r\n" b"Content-Type: text/html\r\n" b"\r\n" + innerBody2 + b"\r\n--xyz--\r\n", ) return d.addCallback(cbProduced) def test_multiPartNoBoundary(self): """ A boundary is generated if none is provided. """ outerBody = b"" innerBody = b"Contained body message text. Squarge." headers = OrderedDict() headers["from"] = "sender@host" headers["to"] = "recipient@domain" headers["subject"] = "booga booga boo" headers["content-type"] = "multipart/alternative" innerHeaders = OrderedDict() innerHeaders["subject"] = "this is subject text" innerHeaders["content-type"] = "text/plain" msg = FakeyMessage( headers, (), None, outerBody, 123, [FakeyMessage(innerHeaders, (), None, innerBody, None, None)], ) c = BufferingConsumer() p = imap4.MessageProducer(msg) p._uuid4 = lambda: uuid.UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") d = p.beginProducing(c) def cbProduced(result): self.failUnlessIdentical(result, p) self.assertEqual( b"".join(c.buffer), b"{341}\r\n" b"From: sender@host\r\n" b"To: recipient@domain\r\n" b"Subject: booga booga boo\r\n" b"Content-Type: multipart/alternative; boundary=" b'"----=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' b"\r\n" b"\r\n" b"\r\n" b"------=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" b"Subject: this is subject text\r\n" b"Content-Type: text/plain\r\n" b"\r\n" + innerBody + b"\r\n------=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--\r\n", ) return d.addCallback(cbProduced) def test_multiPartNoQuotes(self): """ A boundary without does not have them added. """ outerBody = b"" innerBody = b"Contained body message text. Squarge." headers = OrderedDict() headers["from"] = "sender@host" headers["to"] = "recipient@domain" headers["subject"] = "booga booga boo" headers["content-type"] = "multipart/alternative; boundary=xyz" innerHeaders = OrderedDict() innerHeaders["subject"] = "this is subject text" innerHeaders["content-type"] = "text/plain" msg = FakeyMessage( headers, (), None, outerBody, 123, [FakeyMessage(innerHeaders, (), None, innerBody, None, None)], ) c = BufferingConsumer() p = imap4.MessageProducer(msg) d = p.beginProducing(c) def cbProduced(result): self.failUnlessIdentical(result, p) self.assertEqual( b"".join(c.buffer), b"{237}\r\n" b"From: sender@host\r\n" b"To: recipient@domain\r\n" b"Subject: booga booga boo\r\n" b"Content-Type: multipart/alternative; boundary=" b"xyz" b"\r\n" b"\r\n" b"\r\n" b"--xyz\r\n" b"Subject: this is subject text\r\n" b"Content-Type: text/plain\r\n" b"\r\n" + innerBody + b"\r\n--xyz--\r\n", ) return d.addCallback(cbProduced) class MessageSetTests(SynchronousTestCase): """ Tests for L{MessageSet}. """ def test_equalityIterationAndAddition(self): """ Test the following properties of L{MessageSet} addition and equality: 1. Two empty L{MessageSet}s are equal to each other; 2. A L{MessageSet} is not equal to any other object; 2. Adding a L{MessageSet} and another L{MessageSet} or an L{int} representing a single message or a sequence of L{int}s representing a sequence of message numbers produces a new L{MessageSet} that: 3. Has a length equal to the number of messages within each sequence of message numbers; 4. Yields each message number in ascending order when iterated over; 6. L{MessageSet.add} with a single message or a start and end message satisfies 3 and 4 above. """ m1 = MessageSet() m2 = MessageSet() self.assertEqual(m1, m2) self.assertNotEqual(m1, ()) m1 = m1 + 1 self.assertEqual(len(m1), 1) self.assertEqual(list(m1), [1]) m1 = m1 + (1, 3) self.assertEqual(len(m1), 3) self.assertEqual(list(m1), [1, 2, 3]) m2 = m2 + (1, 3) self.assertEqual(m1, m2) self.assertEqual(list(m1 + m2), [1, 2, 3]) m1.add(5) self.assertEqual(len(m1), 4) self.assertEqual(list(m1), [1, 2, 3, 5]) self.assertNotEqual(m1, m2) m1.add(6, 8) self.assertEqual(len(m1), 7) self.assertEqual(list(m1), [1, 2, 3, 5, 6, 7, 8]) def test_lengthWithWildcardRange(self): """ A L{MessageSet} that has a range that ends with L{None} raises a L{TypeError} when its length is requested. """ self.assertRaises(TypeError, len, MessageSet(1, None)) def test_reprSanity(self): """ L{MessageSet.__repr__} does not raise an exception """ repr(MessageSet(1, 2)) def test_stringRepresentationWithWildcards(self): """ In a L{MessageSet}, in the presence of wildcards, if the highest message id is known, the wildcard should get replaced by that high value. """ inputs = [ imap4.parseIdList(b"*"), imap4.parseIdList(b"1:*"), imap4.parseIdList(b"3:*", 6), imap4.parseIdList(b"*:2", 6), ] outputs = [ "*", "1:*", "3:6", "2:6", ] for i, o in zip(inputs, outputs): self.assertEqual(str(i), o) def test_stringRepresentationWithInversion(self): """ In a L{MessageSet}, inverting the high and low numbers in a range doesn't affect the meaning of the range. For example, 3:2 displays just like 2:3, because according to the RFC they have the same meaning. """ inputs = [ imap4.parseIdList(b"2:3"), imap4.parseIdList(b"3:2"), ] outputs = [ "2:3", "2:3", ] for i, o in zip(inputs, outputs): self.assertEqual(str(i), o) def test_createWithSingleMessageNumber(self): """ Creating a L{MessageSet} with a single message number adds only that message to the L{MessageSet}; its serialized form includes only that message number, its length is one, and it yields only that message number. """ m = MessageSet(1) self.assertEqual(str(m), "1") self.assertEqual(len(m), 1) self.assertEqual(list(m), [1]) def test_createWithSequence(self): """ Creating a L{MessageSet} with both a start and end message number adds the sequence between to the L{MessageSet}; its serialized form consists that range, its length is the length of the sequence, and it yields the message numbers inclusively between the start and end. """ m = MessageSet(1, 10) self.assertEqual(str(m), "1:10") self.assertEqual(len(m), 10) self.assertEqual(list(m), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) def test_createWithSingleWildcard(self): """ Creating a L{MessageSet} with a single L{None}, representing C{*}, adds C{*} to the range; its serialized form includes only C{*}, its length is one, but it cannot be iterated over because its endpoint is unknown. """ m = MessageSet(None) self.assertEqual(str(m), "*") self.assertEqual(len(m), 1) self.assertRaises(TypeError, list, m) def test_setLastSingleWildcard(self): """ Setting L{MessageSet.last} replaces L{None}, representing C{*}, with that number, making that L{MessageSet} iterable. """ singleMessageReplaced = MessageSet(None) singleMessageReplaced.last = 10 self.assertEqual(list(singleMessageReplaced), [10]) rangeReplaced = MessageSet(3, None) rangeReplaced.last = 1 self.assertEqual(list(rangeReplaced), [1, 2, 3]) def test_setLastWithWildcardRange(self): """ Setting L{MessageSet.last} replaces L{None} in all ranges. """ m = MessageSet(1, None) m.add(2, None) m.last = 5 self.assertEqual(list(m), [1, 2, 3, 4, 5]) def test_setLastTwiceFails(self): """ L{MessageSet.last} cannot be set twice. """ m = MessageSet(1, None) m.last = 2 with self.assertRaises(ValueError): m.last = 3 def test_lastOverridesNoneInAdd(self): """ Adding a L{None}, representing C{*}, or a sequence that includes L{None} to a L{MessageSet} whose L{last<MessageSet.last>} property has been set replaces all occurrences of L{None} with the value of L{last<MessageSet.last>}. """ hasLast = MessageSet(1) hasLast.last = 4 hasLast.add(None) self.assertEqual(list(hasLast), [1, 4]) self.assertEqual(list(hasLast + (None, 5)), [1, 4, 5]) hasLast.add(3, None) self.assertEqual(list(hasLast), [1, 3, 4]) def test_getLast(self): """ Accessing L{MessageSet.last} returns the last value. """ m = MessageSet(1, None) m.last = 2 self.assertEqual(m.last, 2) def test_extend(self): """ L{MessageSet.extend} accepts as its arugment an L{int} or L{None}, or a sequence L{int}s or L{None}s of length two, or another L{MessageSet}, combining its argument with its instance's existing ranges. """ extendWithInt = MessageSet() extendWithInt.extend(1) self.assertEqual(list(extendWithInt), [1]) extendWithNone = MessageSet() extendWithNone.extend(None) self.assertEqual(str(extendWithNone), "*") extendWithSequenceOfInts = MessageSet() extendWithSequenceOfInts.extend((1, 3)) self.assertEqual(list(extendWithSequenceOfInts), [1, 2, 3]) extendWithSequenceOfNones = MessageSet() extendWithSequenceOfNones.extend((None, None)) self.assertEqual(str(extendWithSequenceOfNones), "*") extendWithMessageSet = MessageSet() extendWithMessageSet.extend(MessageSet(1, 3)) self.assertEqual(list(extendWithMessageSet), [1, 2, 3]) def test_contains(self): """ A L{MessageSet} contains a number if the number falls within one of its ranges, and raises L{TypeError} if any range contains L{None}. """ hasFive = MessageSet(1, 7) doesNotHaveFive = MessageSet(1, 4) + MessageSet(6, 7) self.assertIn(5, hasFive) self.assertNotIn(5, doesNotHaveFive) hasFiveButHasNone = hasFive + None with self.assertRaises(TypeError): 5 in hasFiveButHasNone hasFiveButHasNoneInSequence = hasFive + (10, 12) hasFiveButHasNoneInSequence.add(8, None) with self.assertRaises(TypeError): 5 in hasFiveButHasNoneInSequence def test_rangesMerged(self): """ Adding a sequence of message numbers to a L{MessageSet} that begins or ends immediately before or after an existing sequence in that L{MessageSet}, or overlaps one, merges the two. """ mergeAfter = MessageSet(1, 3) mergeBefore = MessageSet(6, 8) mergeBetweenSequence = mergeAfter + mergeBefore mergeBetweenNumber = mergeAfter + MessageSet(5, 7) self.assertEqual(list(mergeAfter + (2, 4)), [1, 2, 3, 4]) self.assertEqual(list(mergeAfter + (3, 5)), [1, 2, 3, 4, 5]) self.assertEqual(list(mergeBefore + (5, 7)), [5, 6, 7, 8]) self.assertEqual(list(mergeBefore + (4, 6)), [4, 5, 6, 7, 8]) self.assertEqual(list(mergeBetweenSequence + (3, 5)), [1, 2, 3, 4, 5, 6, 7, 8]) self.assertEqual( list(mergeBetweenNumber + MessageSet(4)), [1, 2, 3, 4, 5, 6, 7] ) def test_seq_rangeExamples(self): """ Test the C{seq-range} examples from Section 9, "Formal Syntax" of RFC 3501:: Example: 2:4 and 4:2 are equivalent and indicate values 2, 3, and 4. Example: a unique identifier sequence range of 3291:* includes the UID of the last message in the mailbox, even if that value is less than 3291. @see: U{http://tools.ietf.org/html/rfc3501#section-9} """ self.assertEqual(MessageSet(2, 4), MessageSet(4, 2)) self.assertEqual(list(MessageSet(2, 4)), [2, 3, 4]) m = MessageSet(3291, None) m.last = 3290 self.assertEqual(list(m), [3290, 3291]) def test_sequence_setExamples(self): """ Test the C{sequence-set} examples from Section 9, "Formal Syntax" of RFC 3501. In particular, L{MessageSet} reorders and coalesces overlaps:: Example: a message sequence number set of 2,4:7,9,12:* for a mailbox with 15 messages is equivalent to 2,4,5,6,7,9,12,13,14,15 Example: a message sequence number set of *:4,5:7 for a mailbox with 10 messages is equivalent to 10,9,8,7,6,5,4,5,6,7 and MAY be reordered and overlap coalesced to be 4,5,6,7,8,9,10. @see: U{http://tools.ietf.org/html/rfc3501#section-9} """ fromFifteenMessages = ( MessageSet(2) + MessageSet(4, 7) + MessageSet(9) + MessageSet(12, None) ) fromFifteenMessages.last = 15 self.assertEqual( ",".join(str(i) for i in fromFifteenMessages), "2,4,5,6,7,9,12,13,14,15" ) fromTenMessages = MessageSet(None, 4) + MessageSet(5, 7) fromTenMessages.last = 10 self.assertEqual(",".join(str(i) for i in fromTenMessages), "4,5,6,7,8,9,10") class IMAP4HelperTests(TestCase): """ Tests for various helper utilities in the IMAP4 module. """ def test_commandRepr(self): """ L{imap4.Command}'s C{repr} does not raise an exception. """ repr(imap4.Command(b"COMMAND", [b"arg"], (b"extra"))) def test_fileProducer(self): b = ((b"x" * 1) + (b"y" * 1) + (b"z" * 1)) * 10 c = BufferingConsumer() f = BytesIO(b) p = imap4.FileProducer(f) d = p.beginProducing(c) def cbProduced(result): self.failUnlessIdentical(result, p) self.assertEqual(b"{%d}\r\n%b" % (len(b), b), b"".join(c.buffer)) return result def cbResume(result): # Calling resumeProducing after completion does not raise # an exception p.resumeProducing() return result d.addCallback(cbProduced) d.addCallback(cbResume) # The second cbProduced ensures calling resumeProducing after # completion does not change the result. return d.addCallback(cbProduced) def test_wildcard(self): cases = [ [ "foo/%gum/bar", ["foo/bar", "oo/lalagum/bar", "foo/gumx/bar", "foo/gum/baz"], ["foo/xgum/bar", "foo/gum/bar"], ], [ "foo/x%x/bar", ["foo", "bar", "fuz fuz fuz", "foo/*/bar", "foo/xyz/bar", "foo/xx/baz"], ["foo/xyx/bar", "foo/xx/bar", "foo/xxxxxxxxxxxxxx/bar"], ], [ "foo/xyz*abc/bar", ["foo/xyz/bar", "foo/abc/bar", "foo/xyzab/cbar", "foo/xyza/bcbar"], ["foo/xyzabc/bar", "foo/xyz/abc/bar", "foo/xyz/123/abc/bar"], ], ] for wildcard, fail, succeed in cases: wildcard = imap4.wildcardToRegexp(wildcard, "/") for x in fail: self.assertFalse(wildcard.match(x)) for x in succeed: self.assertTrue(wildcard.match(x)) def test_wildcardNoDelim(self): cases = [ [ "foo/%gum/bar", ["foo/bar", "oo/lalagum/bar", "foo/gumx/bar", "foo/gum/baz"], ["foo/xgum/bar", "foo/gum/bar", "foo/x/gum/bar"], ], [ "foo/x%x/bar", ["foo", "bar", "fuz fuz fuz", "foo/*/bar", "foo/xyz/bar", "foo/xx/baz"], ["foo/xyx/bar", "foo/xx/bar", "foo/xxxxxxxxxxxxxx/bar", "foo/x/x/bar"], ], [ "foo/xyz*abc/bar", ["foo/xyz/bar", "foo/abc/bar", "foo/xyzab/cbar", "foo/xyza/bcbar"], ["foo/xyzabc/bar", "foo/xyz/abc/bar", "foo/xyz/123/abc/bar"], ], ] for wildcard, fail, succeed in cases: wildcard = imap4.wildcardToRegexp(wildcard, None) for x in fail: self.assertFalse(wildcard.match(x), x) for x in succeed: self.assertTrue(wildcard.match(x), x) def test_headerFormatter(self): """ L{imap4._formatHeaders} accepts a C{dict} of header name/value pairs and returns a string representing those headers in the standard multiline, C{":"}-separated format. """ cases = [ ( {"Header1": "Value1", "Header2": "Value2"}, b"Header2: Value2\r\nHeader1: Value1\r\n", ), ] for input, expected in cases: output = imap4._formatHeaders(input) self.assertEqual( sorted(output.splitlines(True)), sorted(expected.splitlines(True)) ) def test_quotedSplitter(self): cases = [ b"""Hello World""", b'''Hello "World!"''', b'''World "Hello" "How are you?"''', b'''"Hello world" How "are you?"''', b"""foo bar "baz buz" NIL""", b'''foo bar "baz buz" "NIL"''', b"""foo NIL "baz buz" bar""", b"""foo "NIL" "baz buz" bar""", b""""NIL" bar "baz buz" foo""", b'oo \\"oo\\" oo', b'"oo \\"oo\\" oo"', b"oo \t oo", b'"oo \t oo"', b"oo \\t oo", b'"oo \\t oo"', rb"oo \o oo", rb'"oo \o oo"', b"oo \\o oo", b'"oo \\o oo"', ] answers = [ [b"Hello", b"World"], [b"Hello", b"World!"], [b"World", b"Hello", b"How are you?"], [b"Hello world", b"How", b"are you?"], [b"foo", b"bar", b"baz buz", None], [b"foo", b"bar", b"baz buz", b"NIL"], [b"foo", None, b"baz buz", b"bar"], [b"foo", b"NIL", b"baz buz", b"bar"], [b"NIL", b"bar", b"baz buz", b"foo"], [b"oo", b'"oo"', b"oo"], [b'oo "oo" oo'], [b"oo", b"oo"], [b"oo \t oo"], [b"oo", b"\\t", b"oo"], [b"oo \\t oo"], [b"oo", rb"\o", b"oo"], [rb"oo \o oo"], [b"oo", b"\\o", b"oo"], [b"oo \\o oo"], ] errors = [ b'"mismatched quote', b'mismatched quote"', b'mismatched"quote', b'"oops here is" another"', ] for s in errors: self.assertRaises(imap4.MismatchedQuoting, imap4.splitQuoted, s) for case, expected in zip(cases, answers): self.assertEqual(imap4.splitQuoted(case), expected) def test_stringCollapser(self): cases = [ [b"a", b"b", b"c", b"d", b"e"], [b"a", b" ", b'"', b"b", b"c", b" ", b'"', b" ", b"d", b"e"], [[b"a", b"b", b"c"], b"d", b"e"], [b"a", [b"b", b"c", b"d"], b"e"], [b"a", b"b", [b"c", b"d", b"e"]], [b'"', b"a", b" ", b'"', [b"b", b"c", b"d"], b'"', b" ", b"e", b'"'], [b"a", [b'"', b" ", b"b", b"c", b" ", b" ", b'"'], b"d", b"e"], ] answers = [ [b"abcde"], [b"a", b"bc ", b"de"], [[b"abc"], b"de"], [b"a", [b"bcd"], b"e"], [b"ab", [b"cde"]], [b"a ", [b"bcd"], b" e"], [b"a", [b" bc "], b"de"], ] for case, expected in zip(cases, answers): self.assertEqual(imap4.collapseStrings(case), expected) def test_parenParser(self): s = b"\r\n".join([b"xx"] * 4) def check(case, expected): parsed = imap4.parseNestedParens(case) self.assertEqual(parsed, [expected]) # XXX This code used to work, but changes occurred within the # imap4.py module which made it no longer necessary for *all* of it # to work. In particular, only the part that makes # 'BODY.PEEK[HEADER.FIELDS.NOT (Subject Bcc Cc)]' come out # correctly no longer needs to work. So, I am loathe to delete the # entire section of the test. --exarkun # self.assertEqual(b'(' + imap4.collapseNestedLists(parsed) + b')', # expected) check( b"(BODY.PEEK[HEADER.FIELDS.NOT (subject bcc cc)] {%d}\r\n%b)" % (len(s), s), [b"BODY.PEEK", [b"HEADER.FIELDS.NOT", [b"subject", b"bcc", b"cc"]], s], ) check( b'(FLAGS (\\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" ' b"RFC822.SIZE 4286 ENVELOPE " b'("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" ' b'"IMAP4rev1 WG mtg summary and minutes" ' b'(("Terry Gray" NIL gray cac.washington.edu)) ' b'(("Terry Gray" NIL gray cac.washington.edu)) ' b'(("Terry Gray" NIL gray cac.washington.edu)) ' b"((NIL NIL imap cac.washington.edu)) " b"((NIL NIL minutes CNRI.Reston.VA.US) " b'("John Klensin" NIL KLENSIN INFOODS.MIT.EDU)) NIL NIL ' b"<B27397-0100000@cac.washington.edu>) " b"BODY (TEXT PLAIN (CHARSET US-ASCII) NIL NIL 7BIT 3028 92))", [ b"FLAGS", [rb"\Seen"], b"INTERNALDATE", b"17-Jul-1996 02:44:25 -0700", b"RFC822.SIZE", b"4286", b"ENVELOPE", [ b"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)", b"IMAP4rev1 WG mtg summary and minutes", [[b"Terry Gray", None, b"gray", b"cac.washington.edu"]], [[b"Terry Gray", None, b"gray", b"cac.washington.edu"]], [[b"Terry Gray", None, b"gray", b"cac.washington.edu"]], [[None, None, b"imap", b"cac.washington.edu"]], [ [None, None, b"minutes", b"CNRI.Reston.VA.US"], [b"John Klensin", None, b"KLENSIN", b"INFOODS.MIT.EDU"], ], None, None, b"<B27397-0100000@cac.washington.edu>", ], b"BODY", [ b"TEXT", b"PLAIN", [b"CHARSET", b"US-ASCII"], None, None, b"7BIT", b"3028", b"92", ], ], ) check(b'("oo \\"oo\\" oo")', [b'oo "oo" oo']) check(b'("oo \\\\ oo")', [b"oo \\\\ oo"]) check(b'("oo \\ oo")', [b"oo \\ oo"]) check(b'("oo \\o")', [b"oo \\o"]) check(rb'("oo \o")', [rb"oo \o"]) check(rb"(oo \o)", [b"oo", rb"\o"]) check(b"(oo \\o)", [b"oo", b"\\o"]) def test_fetchParserSimple(self): cases = [ ["ENVELOPE", "Envelope", "envelope"], ["FLAGS", "Flags", "flags"], ["INTERNALDATE", "InternalDate", "internaldate"], ["RFC822.HEADER", "RFC822Header", "rfc822.header"], ["RFC822.SIZE", "RFC822Size", "rfc822.size"], ["RFC822.TEXT", "RFC822Text", "rfc822.text"], ["RFC822", "RFC822", "rfc822"], ["UID", "UID", "uid"], ["BODYSTRUCTURE", "BodyStructure", "bodystructure"], ] for inp, outp, asString in cases: inp = inp.encode("ascii") p = imap4._FetchParser() p.parseString(inp) self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], getattr(p, outp))) self.assertEqual(str(p.result[0]), asString) def test_fetchParserMacros(self): cases = [ [b"ALL", (4, [b"flags", b"internaldate", b"rfc822.size", b"envelope"])], [ b"FULL", (5, [b"flags", b"internaldate", b"rfc822.size", b"envelope", b"body"]), ], [b"FAST", (3, [b"flags", b"internaldate", b"rfc822.size"])], ] for inp, outp in cases: p = imap4._FetchParser() p.parseString(inp) self.assertEqual(len(p.result), outp[0]) expectedResult = [str(token).lower().encode("ascii") for token in p.result] expectedResult.sort() outp[1].sort() self.assertEqual(expectedResult, outp[1]) def test_fetchParserBody(self): P = imap4._FetchParser p = P() p.parseString(b"BODY") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, False) self.assertEqual(p.result[0].header, None) self.assertEqual(str(p.result[0]), "BODY") p = P() p.parseString(b"BODY.PEEK") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, True) self.assertEqual(str(p.result[0]), "BODY") p = P() p.parseString(b"BODY[]") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].empty, True) self.assertEqual(str(p.result[0]), "BODY[]") p = P() p.parseString(b"BODY[HEADER]") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, False) self.assertTrue(isinstance(p.result[0].header, p.Header)) self.assertEqual(p.result[0].header.negate, True) self.assertEqual(p.result[0].header.fields, ()) self.assertEqual(p.result[0].empty, False) self.assertEqual(str(p.result[0]), "BODY[HEADER]") p = P() p.parseString(b"BODY.PEEK[HEADER]") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, True) self.assertTrue(isinstance(p.result[0].header, p.Header)) self.assertEqual(p.result[0].header.negate, True) self.assertEqual(p.result[0].header.fields, ()) self.assertEqual(p.result[0].empty, False) self.assertEqual(str(p.result[0]), "BODY[HEADER]") p = P() p.parseString(b"BODY[HEADER.FIELDS (Subject Cc Message-Id)]") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, False) self.assertTrue(isinstance(p.result[0].header, p.Header)) self.assertEqual(p.result[0].header.negate, False) self.assertEqual(p.result[0].header.fields, [b"SUBJECT", b"CC", b"MESSAGE-ID"]) self.assertEqual(p.result[0].empty, False) self.assertEqual( bytes(p.result[0]), b"BODY[HEADER.FIELDS (Subject Cc Message-Id)]" ) p = P() p.parseString(b"BODY.PEEK[HEADER.FIELDS (Subject Cc Message-Id)]") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, True) self.assertTrue(isinstance(p.result[0].header, p.Header)) self.assertEqual(p.result[0].header.negate, False) self.assertEqual(p.result[0].header.fields, [b"SUBJECT", b"CC", b"MESSAGE-ID"]) self.assertEqual(p.result[0].empty, False) self.assertEqual( bytes(p.result[0]), b"BODY[HEADER.FIELDS (Subject Cc Message-Id)]" ) p = P() p.parseString(b"BODY.PEEK[HEADER.FIELDS.NOT (Subject Cc Message-Id)]") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, True) self.assertTrue(isinstance(p.result[0].header, p.Header)) self.assertEqual(p.result[0].header.negate, True) self.assertEqual(p.result[0].header.fields, [b"SUBJECT", b"CC", b"MESSAGE-ID"]) self.assertEqual(p.result[0].empty, False) self.assertEqual( bytes(p.result[0]), b"BODY[HEADER.FIELDS.NOT (Subject Cc Message-Id)]" ) p = P() p.parseString(b"BODY[1.MIME]<10.50>") self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, False) self.assertTrue(isinstance(p.result[0].mime, p.MIME)) self.assertEqual(p.result[0].part, (0,)) self.assertEqual(p.result[0].partialBegin, 10) self.assertEqual(p.result[0].partialLength, 50) self.assertEqual(p.result[0].empty, False) self.assertEqual(bytes(p.result[0]), b"BODY[1.MIME]<10.50>") p = P() p.parseString( b"BODY.PEEK[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>" ) self.assertEqual(len(p.result), 1) self.assertTrue(isinstance(p.result[0], p.Body)) self.assertEqual(p.result[0].peek, True) self.assertTrue(isinstance(p.result[0].header, p.Header)) self.assertEqual(p.result[0].part, (0, 2, 8, 10)) self.assertEqual(p.result[0].header.fields, [b"MESSAGE-ID", b"DATE"]) self.assertEqual(p.result[0].partialBegin, 103) self.assertEqual(p.result[0].partialLength, 69) self.assertEqual(p.result[0].empty, False) self.assertEqual( bytes(p.result[0]), b"BODY[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>", ) def test_fetchParserQuotedHeader(self): """ Parsing a C{BODY} whose C{HEADER} values require quoting results in a object that perserves that quoting when serialized. """ p = imap4._FetchParser() p.parseString(b"BODY[HEADER.FIELDS ((Quoted)]") self.assertEqual(len(p.result), 1) self.assertEqual(p.result[0].peek, False) self.assertIsInstance(p.result[0], p.Body) self.assertIsInstance(p.result[0].header, p.Header) self.assertEqual(bytes(p.result[0]), b'BODY[HEADER.FIELDS ("(Quoted")]') def test_fetchParserEmptyString(self): """ Parsing an empty string results in no data. """ p = imap4._FetchParser() p.parseString(b"") self.assertFalse(len(p.result)) def test_fetchParserUnknownAttribute(self): """ Parsing a string with an unknown attribute raises an L{Exception}. """ p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"UNKNOWN") def test_fetchParserIncompleteStringEndsInWhitespace(self): """ Parsing a string that prematurely ends in whitespace raises an L{Exception}. """ p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY[HEADER.FIELDS ") def test_fetchParserExpectedWhitespace(self): """ Parsing a string that contains an unexpected character rather than whitespace raises an L{Exception}. """ p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY[HEADER.FIELDS!]") def test_fetchParserTextSection(self): """ A C{BODY} can contain a C{TEXT} section. """ p = imap4._FetchParser() p.parseString(b"BODY[TEXT]") self.assertEqual(len(p.result), 1) self.assertIsInstance(p.result[0], p.Body) self.assertEqual(p.result[0].peek, False) self.assertIsInstance(p.result[0].text, p.Text) self.assertEqual(bytes(p.result[0]), b"BODY[TEXT]") def test_fetchParserUnknownSection(self): """ Parsing a C{BODY} with an unknown section raises an L{Exception}. """ p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY[UNKNOWN]") def test_fetchParserMissingSectionClose(self): """ Parsing a C{BODY} with an unterminated section list raises an L{Exception}. """ p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY[HEADER") p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY[HEADER.FIELDS (SUBJECT)") def test_fetchParserHeaderMissingParentheses(self): """ Parsing a C{BODY} whose C{HEADER.FIELDS} list does not begin with an open parenthesis (C{(}) or end with a close parenthesis (C{)}) raises an L{Exception}. """ p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY[HEADER.FIELDS Missing)]") p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY[HEADER.FIELDS (Missing]") def test_fetchParserDotlessPartial(self): """ Parsing a C{BODY} with a range that lacks a period (C{.}) raises an L{Exception}. """ p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY<01>") def test_fetchParserUnclosedPartial(self): """ Parsing a C{BODY} with a partial range that's missing its closing greater than sign (C{>}) raises an L{EXCEPTION}. """ p = imap4._FetchParser() self.assertRaises(Exception, p.parseString, b"BODY<0") def test_files(self): inputStructure = [ "foo", "bar", "baz", BytesIO(b"this is a file\r\n"), "buz", "biz", ] output = b'"foo" "bar" "baz" {16}\r\nthis is a file\r\n "buz" "biz"' self.assertEqual(imap4.collapseNestedLists(inputStructure), output) def test_quoteAvoider(self): input = [ b"foo", imap4.DontQuoteMe(b"bar"), b"baz", BytesIO(b"this is a file\r\n"), b"this is\r\nquoted", imap4.DontQuoteMe(b"buz"), b"", ] output = ( b'"foo" bar "baz"' b" {16}\r\nthis is a file\r\n " b"{15}\r\nthis is\r\nquoted" b' buz ""' ) self.assertEqual(imap4.collapseNestedLists(input), output) def test_literals(self): cases = [ (b"({10}\r\n0123456789)", [[b"0123456789"]]), ] for case, expected in cases: self.assertEqual(imap4.parseNestedParens(case), expected) def test_queryBuilder(self): inputs = [ imap4.Query(flagged=1), imap4.Query(sorted=1, unflagged=1, deleted=1), imap4.Or(imap4.Query(flagged=1), imap4.Query(deleted=1)), imap4.Query(before="today"), imap4.Or(imap4.Query(deleted=1), imap4.Query(unseen=1), imap4.Query(new=1)), imap4.Or( imap4.Not( imap4.Or( imap4.Query(sorted=1, since="yesterday", smaller=1000), imap4.Query(sorted=1, before="tuesday", larger=10000), imap4.Query(sorted=1, unseen=1, deleted=1, before="today"), imap4.Not(imap4.Query(subject="spam")), ), ), imap4.Not(imap4.Query(uid="1:5")), ), ] outputs = [ "FLAGGED", "(DELETED UNFLAGGED)", "(OR FLAGGED DELETED)", '(BEFORE "today")', "(OR DELETED (OR UNSEEN NEW))", '(OR (NOT (OR (SINCE "yesterday" SMALLER 1000) ' # Continuing '(OR (BEFORE "tuesday" LARGER 10000) (OR (BEFORE ' # Some more '"today" DELETED UNSEEN) (NOT (SUBJECT "spam")))))) ' # And more "(NOT (UID 1:5)))", ] for query, expected in zip(inputs, outputs): self.assertEqual(query, expected) def test_queryKeywordFlagWithQuotes(self): """ When passed the C{keyword} argument, L{imap4.Query} returns an unquoted string. @see: U{http://tools.ietf.org/html/rfc3501#section-9} @see: U{http://tools.ietf.org/html/rfc3501#section-6.4.4} """ query = imap4.Query(keyword="twisted") self.assertEqual("(KEYWORD twisted)", query) def test_queryUnkeywordFlagWithQuotes(self): """ When passed the C{unkeyword} argument, L{imap4.Query} returns an unquoted string. @see: U{http://tools.ietf.org/html/rfc3501#section-9} @see: U{http://tools.ietf.org/html/rfc3501#section-6.4.4} """ query = imap4.Query(unkeyword="twisted") self.assertEqual("(UNKEYWORD twisted)", query) def test_queryWithMesssageSet(self): """ When passed a L{MessageSet}, L{imap4.Query} returns a query containing a quoted string representing the ID sequence. """ query = imap4.Query(messages=imap4.MessageSet(1, None)) self.assertEqual(query, '(MESSAGES "1:*")') def test_queryWithInteger(self): """ When passed an L{int}, L{imap4.Query} returns a query containing a quoted integer. """ query = imap4.Query(messages=1) self.assertEqual(query, '(MESSAGES "1")') def test_queryOrIllegalQuery(self): """ An L{imap4.Or} query with less than two arguments raises an L{imap4.IllegalQueryError}. """ self.assertRaises(imap4.IllegalQueryError, imap4.Or, imap4.Query(messages=1)) def _keywordFilteringTest(self, keyword): """ Helper to implement tests for value filtering of KEYWORD and UNKEYWORD queries. @param keyword: A native string giving the name of the L{imap4.Query} keyword argument to test. """ # Check all the printable exclusions self.assertEqual( f"({keyword.upper()} twistedrocks)", imap4.Query(**{keyword: r'twisted (){%*"\] rocks'}), ) # Check all the non-printable exclusions self.assertEqual( f"({keyword.upper()} twistedrocks)", imap4.Query( **{ keyword: "twisted %s rocks" % ("".join(chr(ch) for ch in range(33)),) } ), ) def test_queryKeywordFlag(self): r""" When passed the C{keyword} argument, L{imap4.Query} returns an C{atom} that consists of one or more non-special characters. List of the invalid characters: ( ) { % * " \ ] CTL SP @see: U{ABNF definition of CTL and SP<https://tools.ietf.org/html/rfc2234>} @see: U{IMAP4 grammar<http://tools.ietf.org/html/rfc3501#section-9>} @see: U{IMAP4 SEARCH specification<http://tools.ietf.org/html/rfc3501#section-6.4.4>} """ self._keywordFilteringTest("keyword") def test_queryUnkeywordFlag(self): r""" When passed the C{unkeyword} argument, L{imap4.Query} returns an C{atom} that consists of one or more non-special characters. List of the invalid characters: ( ) { % * " \ ] CTL SP @see: U{ABNF definition of CTL and SP<https://tools.ietf.org/html/rfc2234>} @see: U{IMAP4 grammar<http://tools.ietf.org/html/rfc3501#section-9>} @see: U{IMAP4 SEARCH specification<http://tools.ietf.org/html/rfc3501#section-6.4.4>} """ self._keywordFilteringTest("unkeyword") def test_invalidIdListParser(self): """ Trying to parse an invalid representation of a sequence range raises an L{IllegalIdentifierError}. """ inputs = [b"*:*", b"foo", b"4:", b"bar:5"] for input in inputs: self.assertRaises( imap4.IllegalIdentifierError, imap4.parseIdList, input, 12345 ) def test_invalidIdListParserNonPositive(self): """ Zeroes and negative values are not accepted in id range expressions. RFC 3501 states that sequence numbers and sequence ranges consist of non-negative numbers (RFC 3501 section 9, the seq-number grammar item). """ inputs = [b"0:5", b"0:0", b"*:0", b"0", b"-3:5", b"1:-2", b"-1"] for input in inputs: self.assertRaises( imap4.IllegalIdentifierError, imap4.parseIdList, input, 12345 ) def test_parseIdList(self): """ The function to parse sequence ranges yields appropriate L{MessageSet} objects. """ inputs = [ b"1:*", b"5:*", b"1:2,5:*", b"*", b"1", b"1,2", b"1,3,5", b"1:10", b"1:10,11", b"1:5,10:20", b"1,5:10", b"1,5:10,15:20", b"1:10,15,20:25", b"4:2", ] outputs = [ MessageSet(1, None), MessageSet(5, None), MessageSet(5, None) + MessageSet(1, 2), MessageSet(None, None), MessageSet(1), MessageSet(1, 2), MessageSet(1) + MessageSet(3) + MessageSet(5), MessageSet(1, 10), MessageSet(1, 11), MessageSet(1, 5) + MessageSet(10, 20), MessageSet(1) + MessageSet(5, 10), MessageSet(1) + MessageSet(5, 10) + MessageSet(15, 20), MessageSet(1, 10) + MessageSet(15) + MessageSet(20, 25), MessageSet(2, 4), ] lengths = [None, None, None, 1, 1, 2, 3, 10, 11, 16, 7, 13, 17, 3] for input, expected in zip(inputs, outputs): self.assertEqual(imap4.parseIdList(input), expected) for input, expected in zip(inputs, lengths): if expected is None: self.assertRaises(TypeError, len, imap4.parseIdList(input)) else: L = len(imap4.parseIdList(input)) self.assertEqual(L, expected, f"len({input!r}) = {L!r} != {expected!r}") def test_parseTimeInvalidFormat(self): """ L{imap4.parseTime} raises L{ValueError} when given a a time string whose format is invalid. """ self.assertRaises(ValueError, imap4.parseTime, "invalid") def test_parseTimeInvalidValues(self): """ L{imap4.parseTime} raises L{ValueError} when given a time string composed of invalid values. """ invalidStrings = [ "invalid-July-2017", "2-invalid-2017", "2-July-invalid", ] for invalid in invalidStrings: self.assertRaises(ValueError, imap4.parseTime, invalid) def test_statusRequestHelper(self): """ L{imap4.statusRequestHelper} builds a L{dict} mapping the requested status names to values extracted from the provided L{IMailboxIMAP}'s. """ mbox = SimpleMailbox() expected = { "MESSAGES": mbox.getMessageCount(), "RECENT": mbox.getRecentCount(), "UIDNEXT": mbox.getUIDNext(), "UIDVALIDITY": mbox.getUIDValidity(), "UNSEEN": mbox.getUnseenCount(), } result = imap4.statusRequestHelper(mbox, expected.keys()) self.assertEqual(expected, result) @implementer(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox) class SimpleMailbox: flags = ("\\Flag1", "Flag2", "\\AnotherSysFlag", "LastFlag") messages: list[tuple[bytes, list[bytes], bytes, int]] = [] mUID = 0 rw = 1 closed = False def __init__(self): self.listeners = [] self.addListener = self.listeners.append self.removeListener = self.listeners.remove def getFlags(self): return self.flags def getUIDValidity(self): return 42 def getUIDNext(self): return len(self.messages) + 1 def getMessageCount(self): return 9 def getRecentCount(self): return 3 def getUnseenCount(self): return 4 def isWriteable(self): return self.rw def destroy(self): pass def getHierarchicalDelimiter(self): return "/" def requestStatus(self, names): r = {} if "MESSAGES" in names: r["MESSAGES"] = self.getMessageCount() if "RECENT" in names: r["RECENT"] = self.getRecentCount() if "UIDNEXT" in names: r["UIDNEXT"] = self.getMessageCount() + 1 if "UIDVALIDITY" in names: r["UIDVALIDITY"] = self.getUID() if "UNSEEN" in names: r["UNSEEN"] = self.getUnseenCount() return defer.succeed(r) def addMessage(self, message, flags, date=None): self.messages.append((message, flags, date, self.mUID)) self.mUID += 1 return defer.succeed(None) def expunge(self): delete = [] for i in self.messages: if "\\Deleted" in i[1]: delete.append(i) for i in delete: self.messages.remove(i) return [i[3] for i in delete] def close(self): self.closed = True def fetch(self, messages, uid): # IMailboxIMAP.fetch pass def getUID(self, message): # IMailboxIMAP.getUID pass def store(self, messages, flags, mode, uid): # IMailboxIMAP.store pass @implementer(imap4.IMailboxInfo, imap4.IMailbox) class UncloseableMailbox: """ A mailbox that cannot be closed. """ flags = ("\\Flag1", "Flag2", "\\AnotherSysFlag", "LastFlag") messages: list[tuple[bytes, list[bytes], bytes, int]] = [] mUID = 0 rw = 1 closed = False def __init__(self): self.listeners = [] self.addListener = self.listeners.append self.removeListener = self.listeners.remove def getFlags(self): """ The flags @return: A sequence of flags. """ return self.flags def getUIDValidity(self): """ The UID validity value. @return: The value. """ return 42 def getUIDNext(self): """ The next UID. @return: The UID. """ return len(self.messages) + 1 def getMessageCount(self): """ The number of messages. @return: The number. """ return 9 def getRecentCount(self): """ The recent messages. @return: The number. """ return 3 def getUnseenCount(self): """ The recent messages. @return: The number. """ return 4 def isWriteable(self): """ The recent messages. @return: Whether or not the mailbox is writable. """ return self.rw def destroy(self): """ Destroy this mailbox. """ pass def getHierarchicalDelimiter(self): """ Return the hierarchical delimiter. @return: The delimiter. """ return "/" def requestStatus(self, names): """ Return the mailbox's status. @param names: The status items to include. @return: A L{dict} of status data. """ r = {} if "MESSAGES" in names: r["MESSAGES"] = self.getMessageCount() if "RECENT" in names: r["RECENT"] = self.getRecentCount() if "UIDNEXT" in names: r["UIDNEXT"] = self.getMessageCount() + 1 if "UIDVALIDITY" in names: r["UIDVALIDITY"] = self.getUID() if "UNSEEN" in names: r["UNSEEN"] = self.getUnseenCount() return defer.succeed(r) def addMessage(self, message, flags, date=None): """ Add a message to the mailbox. @param message: The message body. @param flags: The message flags. @param date: The message date. @return: A L{Deferred} that fires when the message has been added. """ self.messages.append((message, flags, date, self.mUID)) self.mUID += 1 return defer.succeed(None) def expunge(self): """ Delete messages marked for deletion. @return: A L{list} of deleted message IDs. """ delete = [] for i in self.messages: if "\\Deleted" in i[1]: delete.append(i) for i in delete: self.messages.remove(i) return [i[3] for i in delete] def fetch(self, messages, uid): # IMailboxIMAP.fetch pass def getUID(self, message): # IMailboxIMAP.getUID pass def store(self, messages, flags, mode, uid): # IMailboxIMAP.store pass class AccountWithoutNamespaces(imap4.MemoryAccountWithoutNamespaces): """ An in-memory account that does not provide L{INamespacePresenter}. """ mailboxFactory = SimpleMailbox def _emptyMailbox(self, name, id): return self.mailboxFactory() def select(self, name, rw=1): mbox = imap4.MemoryAccount.select(self, name) if mbox is not None: mbox.rw = rw return mbox class Account(AccountWithoutNamespaces, imap4.MemoryAccount): """ An in-memory account that provides L{INamespacePresenter}. """ class SimpleServer(imap4.IMAP4Server): theAccount = Account(b"testuser") def __init__(self, *args, **kw): imap4.IMAP4Server.__init__(self, *args, **kw) realm = TestRealm(accountHolder=self) portal = Portal(realm) c = InMemoryUsernamePasswordDatabaseDontUse() c.addUser(b"testuser", b"password-test") self.checker = c self.portal = portal portal.registerChecker(c) self.timeoutTest = False def lineReceived(self, line): if self.timeoutTest: # Do not send a response return imap4.IMAP4Server.lineReceived(self, line) class SimpleClient(imap4.IMAP4Client): def __init__(self, deferred, contextFactory=None): imap4.IMAP4Client.__init__(self, contextFactory) self.deferred = deferred self.events = [] def serverGreeting(self, caps): self.deferred.callback(None) def modeChanged(self, writeable): self.events.append(["modeChanged", writeable]) self.transport.loseConnection() def flagsChanged(self, newFlags): self.events.append(["flagsChanged", newFlags]) self.transport.loseConnection() def newMessages(self, exists, recent): self.events.append(["newMessages", exists, recent]) self.transport.loseConnection() class IMAP4HelperMixin: serverCTX: Optional[ServerTLSContext] = None clientCTX: Optional[ClientTLSContext] = None def setUp(self): d = defer.Deferred() self.server = SimpleServer(contextFactory=self.serverCTX) self.client = SimpleClient(d, contextFactory=self.clientCTX) self.connected = d SimpleMailbox.messages = [] theAccount = Account(b"testuser") theAccount.mboxType = SimpleMailbox SimpleServer.theAccount = theAccount def tearDown(self): del self.server del self.client del self.connected def _cbStopClient(self, ignore): self.client.transport.loseConnection() def _ebGeneral(self, failure): self.client.transport.loseConnection() self.server.transport.loseConnection() log.err(failure, "Problem with " + str(self)) def loopback(self): return loopback.loopbackAsync(self.server, self.client) def assertClientFailureMessage(self, failure, expected): """ Assert that the provided failure is an L{IMAP4Exception} with the given message. @param failure: A failure whose value L{IMAP4Exception} @type failure: L{failure.Failure} @param expected: The expected failure message. @type expected: L{bytes} """ failure.trap(imap4.IMAP4Exception) message = str(failure.value) expected = repr(expected) self.assertEqual(message, expected) class IMAP4ServerTests(IMAP4HelperMixin, TestCase): def testCapability(self): caps = {} def getCaps(): def gotCaps(c): caps.update(c) self.server.transport.loseConnection() return self.client.getCapabilities().addCallback(gotCaps) d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) expected = {b"IMAP4rev1": None, b"NAMESPACE": None, b"IDLE": None} return d.addCallback(lambda _: self.assertEqual(expected, caps)) def testCapabilityWithAuth(self): caps = {} self.server.challengers[b"CRAM-MD5"] = CramMD5Credentials def getCaps(): def gotCaps(c): caps.update(c) self.server.transport.loseConnection() return self.client.getCapabilities().addCallback(gotCaps) d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) expCap = { b"IMAP4rev1": None, b"NAMESPACE": None, b"IDLE": None, b"AUTH": [b"CRAM-MD5"], } return d.addCallback(lambda _: self.assertEqual(expCap, caps)) def testLogout(self): self.loggedOut = 0 def logout(): def setLoggedOut(): self.loggedOut = 1 self.client.logout().addCallback(strip(setLoggedOut)) self.connected.addCallback(strip(logout)).addErrback(self._ebGeneral) d = self.loopback() return d.addCallback(lambda _: self.assertEqual(self.loggedOut, 1)) def testNoop(self): self.responses = None def noop(): def setResponses(responses): self.responses = responses self.server.transport.loseConnection() self.client.noop().addCallback(setResponses) self.connected.addCallback(strip(noop)).addErrback(self._ebGeneral) d = self.loopback() return d.addCallback(lambda _: self.assertEqual(self.responses, [])) def testLogin(self): def login(): d = self.client.login(b"testuser", b"password-test") d.addCallback(self._cbStopClient) d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) d = defer.gatherResults([d1, self.loopback()]) return d.addCallback(self._cbTestLogin) def _cbTestLogin(self, ignored): self.assertEqual(self.server.account, SimpleServer.theAccount) self.assertEqual(self.server.state, "auth") def testFailedLogin(self): def login(): d = self.client.login(b"testuser", b"wrong-password") d.addBoth(self._cbStopClient) d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestFailedLogin) def _cbTestFailedLogin(self, ignored): self.assertEqual(self.server.account, None) self.assertEqual(self.server.state, "unauth") def test_loginWithoutPortal(self): """ Attempting to log into a server that has no L{Portal} results in a failed login. """ self.server.portal = None def login(): d = self.client.login(b"testuser", b"wrong-password") d.addBoth(self._cbStopClient) d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestFailedLogin) def test_nonIAccountAvatar(self): """ The server responds with a C{BAD} response when its portal attempts to log a user in with checker that claims to support L{IAccount} but returns an an avatar interface that is not L{IAccount}. """ def brokenRequestAvatar(*_, **__): return ("Not IAccount", "Not an account", lambda: None) self.server.portal.realm.requestAvatar = brokenRequestAvatar def login(): d = self.client.login(b"testuser", b"password-test") d.addBoth(self._cbStopClient) d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestFailedLogin) def test_loginException(self): """ Any exception raised by L{IMAP4Server.authenticateLogin} that is not L{UnauthorizedLogin} is logged results in a C{BAD} response. """ class UnexpectedException(Exception): """ An unexpected exception. """ def raisesUnexpectedException(user, passwd): raise UnexpectedException("Whoops") self.server.authenticateLogin = raisesUnexpectedException def login(): return self.client.login(b"testuser", b"password-test") d1 = self.connected.addCallback(strip(login)) d1.addErrback(self.assertClientFailureMessage, b"Server error: Whoops") @d1.addCallback def assertErrorLogged(_): self.assertTrue(self.flushLoggedErrors(UnexpectedException)) d1.addErrback(self._ebGeneral) d1.addBoth(self._cbStopClient) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestFailedLogin) def testLoginRequiringQuoting(self): self.server.checker.users = {b"{test}user": b"{test}password"} def login(): d = self.client.login(b"{test}user", b"{test}password") d.addErrback(log.err, "Problem with " + str(self)) d.addCallback(self._cbStopClient) d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestLoginRequiringQuoting) def _cbTestLoginRequiringQuoting(self, ignored): self.assertEqual(self.server.account, SimpleServer.theAccount) self.assertEqual(self.server.state, "auth") def testNamespace(self): self.namespaceArgs = None def login(): return self.client.login(b"testuser", b"password-test") def namespace(): def gotNamespace(args): self.namespaceArgs = args self._cbStopClient(None) return self.client.namespace().addCallback(gotNamespace) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(namespace)) d1.addErrback(self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) @d.addCallback def assertAllPairsNativeStrings(ignored): for namespaces in self.namespaceArgs: for pair in namespaces: for value in pair: self.assertIsInstance(value, str) return self.namespaceArgs d.addCallback(self.assertEqual, [[["", "/"]], [], []]) return d def test_mailboxWithoutNamespace(self): """ A mailbox that does not provide L{INamespacePresenter} returns empty L{list}s for its personal, shared, and user namespaces. """ self.server.theAccount = AccountWithoutNamespaces(b"testuser") self.namespaceArgs = None def login(): return self.client.login(b"testuser", b"password-test") def namespace(): def gotNamespace(args): self.namespaceArgs = args self._cbStopClient(None) return self.client.namespace().addCallback(gotNamespace) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(namespace)) d1.addErrback(self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback(lambda _: self.namespaceArgs) d.addCallback(self.assertEqual, [[], [], []]) return d def testSelect(self): SimpleServer.theAccount.addMailbox("test-mailbox") self.selectedArgs = None def login(): return self.client.login(b"testuser", b"password-test") def select(): def selected(args): self.selectedArgs = args self._cbStopClient(None) d = self.client.select("test-mailbox") d.addCallback(selected) return d d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(select)) d1.addErrback(self._ebGeneral) d2 = self.loopback() return defer.gatherResults([d1, d2]).addCallback(self._cbTestSelect) def test_selectWithoutMailbox(self): """ A client that selects a mailbox that does not exist receives a C{NO} response. """ def login(): return self.client.login(b"testuser", b"password-test") def select(): return self.client.select("test-mailbox") self.connected.addCallback(strip(login)) self.connected.addCallback(strip(select)) self.connected.addErrback(self.assertClientFailureMessage, b"No such mailbox") self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) connectionComplete = defer.gatherResults([self.connected, self.loopback()]) @connectionComplete.addCallback def assertNoMailboxSelected(_): self.assertIsNone(self.server.mbox) return connectionComplete def _cbTestSelect(self, ignored): mbox = SimpleServer.theAccount.mailboxes["TEST-MAILBOX"] self.assertEqual(self.server.mbox, mbox) self.assertEqual( self.selectedArgs, { "EXISTS": 9, "RECENT": 3, "UIDVALIDITY": 42, "FLAGS": ("\\Flag1", "Flag2", "\\AnotherSysFlag", "LastFlag"), "READ-WRITE": True, }, ) def test_examine(self): """ L{IMAP4Client.examine} issues an I{EXAMINE} command to the server and returns a L{Deferred} which fires with a C{dict} with as many of the following keys as the server includes in its response: C{'FLAGS'}, C{'EXISTS'}, C{'RECENT'}, C{'UNSEEN'}, C{'READ-WRITE'}, C{'READ-ONLY'}, C{'UIDVALIDITY'}, and C{'PERMANENTFLAGS'}. Unfortunately the server doesn't generate all of these so it's hard to test the client's handling of them here. See L{IMAP4ClientExamineTests} below. See U{RFC 3501<http://www.faqs.org/rfcs/rfc3501.html>}, section 6.3.2, for details. """ SimpleServer.theAccount.addMailbox("test-mailbox") self.examinedArgs = None def login(): return self.client.login(b"testuser", b"password-test") def examine(): def examined(args): self.examinedArgs = args self._cbStopClient(None) d = self.client.examine("test-mailbox") d.addCallback(examined) return d d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(examine)) d1.addErrback(self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestExamine) def _cbTestExamine(self, ignored): mbox = SimpleServer.theAccount.mailboxes["TEST-MAILBOX"] self.assertEqual(self.server.mbox, mbox) self.assertEqual( self.examinedArgs, { "EXISTS": 9, "RECENT": 3, "UIDVALIDITY": 42, "FLAGS": ("\\Flag1", "Flag2", "\\AnotherSysFlag", "LastFlag"), "READ-WRITE": False, }, ) def testCreate(self): succeed = ("testbox", "test/box", "test/", "test/box/box", "INBOX") fail = ("testbox", "test/box") def cb(): self.result.append(1) def eb(failure): self.result.append(0) def login(): return self.client.login(b"testuser", b"password-test") def create(): for name in succeed + fail: d = self.client.create(name) d.addCallback(strip(cb)).addErrback(eb) d.addCallbacks(self._cbStopClient, self._ebGeneral) self.result = [] d1 = self.connected.addCallback(strip(login)).addCallback(strip(create)) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestCreate, succeed, fail) def _cbTestCreate(self, ignored, succeed, fail): self.assertEqual(self.result, [1] * len(succeed) + [0] * len(fail)) mbox = sorted(SimpleServer.theAccount.mailboxes) answers = sorted(["inbox", "testbox", "test/box", "test", "test/box/box"]) self.assertEqual(mbox, [a.upper() for a in answers]) def testDelete(self): SimpleServer.theAccount.addMailbox("delete/me") def login(): return self.client.login(b"testuser", b"password-test") def delete(): return self.client.delete("delete/me") d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(delete), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback( lambda _: self.assertEqual(list(SimpleServer.theAccount.mailboxes), []) ) return d def testDeleteWithInferiorHierarchicalNames(self): """ Attempting to delete a mailbox with hierarchically inferior names fails with an informative error. @see: U{https://tools.ietf.org/html/rfc3501#section-6.3.4} @return: A L{Deferred} with assertions. """ SimpleServer.theAccount.addMailbox("delete") SimpleServer.theAccount.addMailbox("delete/me") def login(): return self.client.login(b"testuser", b"password-test") def delete(): return self.client.delete("delete") def assertIMAPException(failure): failure.trap(imap4.IMAP4Exception) self.assertEqual( str(failure.value), str(b'Name "DELETE" has inferior hierarchical names'), ) loggedIn = self.connected.addCallback(strip(login)) loggedIn.addCallbacks(strip(delete), self._ebGeneral) loggedIn.addErrback(assertIMAPException) loggedIn.addCallbacks(self._cbStopClient) loopedBack = self.loopback() d = defer.gatherResults([loggedIn, loopedBack]) d.addCallback( lambda _: self.assertEqual( sorted(SimpleServer.theAccount.mailboxes), ["DELETE", "DELETE/ME"] ) ) return d def testIllegalInboxDelete(self): self.stashed = None def login(): return self.client.login(b"testuser", b"password-test") def delete(): return self.client.delete("inbox") def stash(result): self.stashed = result d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(delete), self._ebGeneral) d1.addBoth(stash) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback( lambda _: self.assertTrue(isinstance(self.stashed, failure.Failure)) ) return d def testNonExistentDelete(self): def login(): return self.client.login(b"testuser", b"password-test") def delete(): return self.client.delete("delete/me") def deleteFailed(failure): self.failure = failure self.failure = None d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(delete)).addErrback(deleteFailed) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback( lambda _: self.assertEqual(str(self.failure.value), str(b"No such mailbox")) ) return d def testIllegalDelete(self): m = SimpleMailbox() m.flags = (r"\Noselect",) SimpleServer.theAccount.addMailbox("delete", m) SimpleServer.theAccount.addMailbox("delete/me") def login(): return self.client.login(b"testuser", b"password-test") def delete(): return self.client.delete("delete") def deleteFailed(failure): self.failure = failure self.failure = None d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(delete)).addErrback(deleteFailed) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) expected = str( b"Hierarchically inferior mailboxes exist " b"and \\Noselect is set" ) d.addCallback(lambda _: self.assertEqual(str(self.failure.value), expected)) return d def testRename(self): SimpleServer.theAccount.addMailbox("oldmbox") def login(): return self.client.login(b"testuser", b"password-test") def rename(): return self.client.rename(b"oldmbox", b"newname") d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(rename), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback( lambda _: self.assertEqual( list(SimpleServer.theAccount.mailboxes.keys()), ["NEWNAME"] ) ) return d def testIllegalInboxRename(self): self.stashed = None def login(): return self.client.login(b"testuser", b"password-test") def rename(): return self.client.rename("inbox", "frotz") def stash(stuff): self.stashed = stuff d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(rename), self._ebGeneral) d1.addBoth(stash) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback( lambda _: self.assertTrue(isinstance(self.stashed, failure.Failure)) ) return d def testHierarchicalRename(self): SimpleServer.theAccount.create("oldmbox/m1") SimpleServer.theAccount.create("oldmbox/m2") def login(): return self.client.login(b"testuser", b"password-test") def rename(): return self.client.rename("oldmbox", "newname") d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(rename), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestHierarchicalRename) def _cbTestHierarchicalRename(self, ignored): mboxes = SimpleServer.theAccount.mailboxes.keys() expected = ["newname", "newname/m1", "newname/m2"] mboxes = list(sorted(mboxes)) self.assertEqual(mboxes, [s.upper() for s in expected]) def testSubscribe(self): def login(): return self.client.login(b"testuser", b"password-test") def subscribe(): return self.client.subscribe("this/mbox") d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(subscribe), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback( lambda _: self.assertEqual( SimpleServer.theAccount.subscriptions, ["THIS/MBOX"] ) ) return d def testUnsubscribe(self): SimpleServer.theAccount.subscriptions = ["THIS/MBOX", "THAT/MBOX"] def login(): return self.client.login(b"testuser", b"password-test") def unsubscribe(): return self.client.unsubscribe("this/mbox") d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(unsubscribe), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback( lambda _: self.assertEqual( SimpleServer.theAccount.subscriptions, ["THAT/MBOX"] ) ) return d def _listSetup(self, f): SimpleServer.theAccount.addMailbox("root/subthing") SimpleServer.theAccount.addMailbox("root/another-thing") SimpleServer.theAccount.addMailbox("non-root/subthing") def login(): return self.client.login(b"testuser", b"password-test") def listed(answers): self.listed = answers self.listed = None d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(f), self._ebGeneral) d1.addCallbacks(listed, self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() return defer.gatherResults([d1, d2]).addCallback(lambda _: self.listed) def assertListDelimiterAndMailboxAreStrings(self, results): """ Assert a C{LIST} response's delimiter and mailbox are native strings. @param results: A list of tuples as returned by L{IMAP4Client.list} or L{IMAP4Client.lsub}. """ for result in results: self.assertIsInstance(result[1], str, "delimiter %r is not a str") self.assertIsInstance(result[2], str, "mailbox %r is not a str") return results def testList(self): def mailboxList(): return self.client.list("root", "%") d = self._listSetup(mailboxList) @d.addCallback def assertListContents(listed): expectedContents = [ (sorted(SimpleMailbox.flags), "/", "ROOT/SUBTHING"), (sorted(SimpleMailbox.flags), "/", "ROOT/ANOTHER-THING"), ] for _ in range(2): flags, delimiter, mailbox = listed.pop(0) self.assertIn( (sorted(flags), delimiter, mailbox), expectedContents, ) self.assertFalse(listed, f"More results than expected: {listed!r}") return d def testLSub(self): SimpleServer.theAccount.subscribe("ROOT/SUBTHING") def lsub(): return self.client.lsub("root", "%") d = self._listSetup(lsub) d.addCallback(self.assertListDelimiterAndMailboxAreStrings) d.addCallback(self.assertEqual, [(SimpleMailbox.flags, "/", "ROOT/SUBTHING")]) return d def testStatus(self): SimpleServer.theAccount.addMailbox("root/subthing") def login(): return self.client.login(b"testuser", b"password-test") def status(): return self.client.status("root/subthing", "MESSAGES", "UIDNEXT", "UNSEEN") def statused(result): self.statused = result self.statused = None d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(status), self._ebGeneral) d1.addCallbacks(statused, self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) d.addCallback( lambda _: self.assertEqual( self.statused, {"MESSAGES": 9, "UIDNEXT": b"10", "UNSEEN": 4} ) ) return d def testFailedStatus(self): def login(): return self.client.login(b"testuser", b"password-test") def status(): return self.client.status( "root/nonexistent", "MESSAGES", "UIDNEXT", "UNSEEN" ) def statused(result): self.statused = result def failed(failure): self.failure = failure self.statused = self.failure = None d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(status), self._ebGeneral) d1.addCallbacks(statused, failed) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() return defer.gatherResults([d1, d2]).addCallback(self._cbTestFailedStatus) def _cbTestFailedStatus(self, ignored): self.assertEqual(self.statused, None) self.assertEqual(self.failure.value.args, (b"Could not open mailbox",)) def testFullAppend(self): infile = util.sibpath(__file__, "rfc822.message") SimpleServer.theAccount.addMailbox("root/subthing") def login(): return self.client.login(b"testuser", b"password-test") @defer.inlineCallbacks def append(): with open(infile, "rb") as message: result = yield self.client.append( "root/subthing", message, ("\\SEEN", "\\DELETED"), "Tue, 17 Jun 2003 11:22:16 -0600 (MDT)", ) defer.returnValue(result) d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(append), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestFullAppend, infile) def _cbTestFullAppend(self, ignored, infile): mb = SimpleServer.theAccount.mailboxes["ROOT/SUBTHING"] self.assertEqual(1, len(mb.messages)) self.assertEqual( (["\\SEEN", "\\DELETED"], b"Tue, 17 Jun 2003 11:22:16 -0600 (MDT)", 0), mb.messages[0][1:], ) with open(infile, "rb") as f: self.assertEqual(f.read(), mb.messages[0][0].getvalue()) def testPartialAppend(self): infile = util.sibpath(__file__, "rfc822.message") SimpleServer.theAccount.addMailbox("PARTIAL/SUBTHING") def login(): return self.client.login(b"testuser", b"password-test") @defer.inlineCallbacks def append(): with open(infile, "rb") as message: result = yield self.client.sendCommand( imap4.Command( b"APPEND", # Using networkString is cheating! In this # particular case the mailbox name happens to # be ASCII. In real code, the mailbox would # be encoded with imap4-utf-7. networkString( "PARTIAL/SUBTHING " '(\\SEEN) "Right now" ' "{%d}" % (os.path.getsize(infile),) ), (), self.client._IMAP4Client__cbContinueAppend, message, ) ) defer.returnValue(result) d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(append), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestPartialAppend, infile) def _cbTestPartialAppend(self, ignored, infile): mb = SimpleServer.theAccount.mailboxes["PARTIAL/SUBTHING"] self.assertEqual(1, len(mb.messages)) self.assertEqual((["\\SEEN"], b"Right now", 0), mb.messages[0][1:]) with open(infile, "rb") as f: self.assertEqual(f.read(), mb.messages[0][0].getvalue()) def _testCheck(self): SimpleServer.theAccount.addMailbox(b"root/subthing") def login(): return self.client.login(b"testuser", b"password-test") def select(): return self.client.select(b"root/subthing") def check(): return self.client.check() d = self.connected.addCallback(strip(login)) d.addCallbacks(strip(select), self._ebGeneral) d.addCallbacks(strip(check), self._ebGeneral) d.addCallbacks(self._cbStopClient, self._ebGeneral) return self.loopback() def test_check(self): """ Trigger the L{imap.IMAP4Server._cbSelectWork} callback by selecting an mbox. """ return self._testCheck() def test_checkFail(self): """ Trigger the L{imap.IMAP4Server._ebSelectWork} errback by failing when we select an mbox. """ def failSelect(self, name, rw=1): raise imap4.IllegalMailboxEncoding("encoding") def checkResponse(ignore): failures = self.flushLoggedErrors() self.assertEqual(failures[1].value.args[0], b"SELECT failed: Server error") self.patch(Account, "select", failSelect) d = self._testCheck() return d.addCallback(checkResponse) def testClose(self): m = SimpleMailbox() m.messages = [ (b"Message 1", ("\\Deleted", "AnotherFlag"), None, 0), (b"Message 2", ("AnotherFlag",), None, 1), (b"Message 3", ("\\Deleted",), None, 2), ] SimpleServer.theAccount.addMailbox("mailbox", m) def login(): return self.client.login(b"testuser", b"password-test") def select(): return self.client.select(b"mailbox") def close(): return self.client.close() d = self.connected.addCallback(strip(login)) d.addCallbacks(strip(select), self._ebGeneral) d.addCallbacks(strip(close), self._ebGeneral) d.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() return defer.gatherResults([d, d2]).addCallback(self._cbTestClose, m) def _cbTestClose(self, ignored, m): self.assertEqual(len(m.messages), 1) self.assertEqual(m.messages[0], (b"Message 2", ("AnotherFlag",), None, 1)) self.assertTrue(m.closed) def testExpunge(self): m = SimpleMailbox() m.messages = [ (b"Message 1", ("\\Deleted", "AnotherFlag"), None, 0), (b"Message 2", ("AnotherFlag",), None, 1), (b"Message 3", ("\\Deleted",), None, 2), ] SimpleServer.theAccount.addMailbox("mailbox", m) def login(): return self.client.login(b"testuser", b"password-test") def select(): return self.client.select("mailbox") def expunge(): return self.client.expunge() def expunged(results): self.assertFalse(self.server.mbox is None) self.results = results self.results = None d1 = self.connected.addCallback(strip(login)) d1.addCallbacks(strip(select), self._ebGeneral) d1.addCallbacks(strip(expunge), self._ebGeneral) d1.addCallbacks(expunged, self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestExpunge, m) def _cbTestExpunge(self, ignored, m): self.assertEqual(len(m.messages), 1) self.assertEqual(m.messages[0], (b"Message 2", ("AnotherFlag",), None, 1)) self.assertEqual(self.results, [0, 2]) class IMAP4ServerParsingTests(SynchronousTestCase): """ Test L{imap4.IMAP4Server}'s command parsing. """ def setUp(self): self.transport = StringTransport() self.server = imap4.IMAP4Server() self.server.makeConnection(self.transport) self.transport.clear() def tearDown(self): self.server.connectionLost(failure.Failure(error.ConnectionDone())) def test_parseMethodExceptionLogged(self): """ L{imap4.IMAP4Server} logs exceptions raised by parse methods. """ class UnhandledException(Exception): """ An unhandled exception. """ def raisesValueError(line): raise UnhandledException self.server.parseState = "command" self.server.parse_command = raisesValueError self.server.lineReceived(b"invalid") self.assertTrue(self.flushLoggedErrors(UnhandledException)) def test_missingCommand(self): """ L{imap4.IMAP4Server.parse_command} sends a C{BAD} response to a line that includes a tag but no command. """ self.server.parse_command(b"001") self.assertEqual(self.transport.value(), b"001 BAD Missing command\r\n") self.server.connectionLost( failure.Failure(error.ConnectionDone("Done")), ) def test_emptyLine(self): """ L{imap4.IMAP4Server.parse_command} sends a C{BAD} response to an empty line. """ self.server.parse_command(b"") self.assertEqual(self.transport.value(), b"* BAD Null command\r\n") def assertParseExceptionResponse(self, exception, tag, expectedResponse): """ Assert that the given exception results in the expected response. @param exception: The exception to raise. @type exception: L{Exception} @param tag: The IMAP tag. @type: L{bytes} @param expectedResponse: The expected bad response. @type expectedResponse: L{bytes} """ def raises(tag, cmd, rest): raise exception self.server.dispatchCommand = raises self.server.parse_command(b" ".join([tag, b"invalid"])) self.assertEqual(self.transport.value(), b" ".join([tag, expectedResponse])) def test_parsingRaisesIllegalClientResponse(self): """ When a parsing method raises L{IllegalClientResponse}, the server sends a C{BAD} response. """ self.assertParseExceptionResponse( imap4.IllegalClientResponse("client response"), b"001", b"BAD Illegal syntax: client response\r\n", ) def test_parsingRaisesIllegalOperationResponse(self): """ When a parsing method raises L{IllegalOperation}, the server sends a C{NO} response. """ self.assertParseExceptionResponse( imap4.IllegalOperation("operation"), b"001", b"NO Illegal operation: operation\r\n", ) def test_parsingRaisesIllegalMailboxEncoding(self): """ When a parsing method raises L{IllegalMailboxEncoding}, the server sends a C{NO} response. """ self.assertParseExceptionResponse( imap4.IllegalMailboxEncoding("encoding"), b"001", b"NO Illegal mailbox name: encoding\r\n", ) def test_unsupportedCommand(self): """ L{imap4.IMAP4Server} responds to an unsupported command with a C{BAD} response. """ self.server.lineReceived(b"001 HULLABALOO") self.assertEqual(self.transport.value(), b"001 BAD Unsupported command\r\n") def test_tooManyArgumentsForCommand(self): """ L{imap4.IMAP4Server} responds with a C{BAD} response to a command with more arguments than expected. """ self.server.lineReceived(b"001 LOGIN A B C") self.assertEqual( self.transport.value(), ( b"001 BAD Illegal syntax:" + b" Too many arguments for command: " + repr(b"C").encode("utf-8") + b"\r\n" ), ) def assertCommandExceptionResponse(self, exception, tag, expectedResponse): """ Assert that the given exception results in the expected response. @param exception: The exception to raise. @type exception: L{Exception} @param: The IMAP tag. @type: L{bytes} @param expectedResponse: The expected bad response. @type expectedResponse: L{bytes} """ def raises(serverInstance, tag, user, passwd): raise exception self.assertEqual(self.server.state, "unauth") self.server.unauth_LOGIN = (raises,) + self.server.unauth_LOGIN[1:] self.server.dispatchCommand(tag, b"LOGIN", b"user passwd") self.assertEqual(self.transport.value(), b" ".join([tag, expectedResponse])) def test_commandRaisesIllegalClientResponse(self): """ When a command raises L{IllegalClientResponse}, the server sends a C{BAD} response. """ self.assertCommandExceptionResponse( imap4.IllegalClientResponse("client response"), b"001", b"BAD Illegal syntax: client response\r\n", ) def test_commandRaisesIllegalOperationResponse(self): """ When a command raises L{IllegalOperation}, the server sends a C{NO} response. """ self.assertCommandExceptionResponse( imap4.IllegalOperation("operation"), b"001", b"NO Illegal operation: operation\r\n", ) def test_commandRaisesIllegalMailboxEncoding(self): """ When a command raises L{IllegalMailboxEncoding}, the server sends a C{NO} response. """ self.assertCommandExceptionResponse( imap4.IllegalMailboxEncoding("encoding"), b"001", b"NO Illegal mailbox name: encoding\r\n", ) def test_commandRaisesUnhandledException(self): """ Wehn a command raises an unhandled exception, the server sends a C{BAD} response and logs the exception. """ class UnhandledException(Exception): """ An unhandled exception. """ self.assertCommandExceptionResponse( UnhandledException("unhandled"), b"001", b"BAD Server error: unhandled\r\n", ) self.assertTrue(self.flushLoggedErrors(UnhandledException)) def test_stringLiteralTooLong(self): """ A string literal whose length exceeds the maximum allowed length results in a C{BAD} response. """ self.server._literalStringLimit = 4 self.server.lineReceived(b"001 LOGIN {5}\r\n") self.assertEqual( self.transport.value(), b"001 BAD Illegal syntax: Literal too long!" b" I accept at most 4 octets\r\n", ) def test_arg_astringEmptyLine(self): """ An empty string argument raises L{imap4.IllegalClientResponse}. """ for empty in [b"", b"\r\n", b" "]: self.assertRaises( imap4.IllegalClientResponse, self.server.arg_astring, empty ) def test_arg_astringUnmatchedQuotes(self): """ An unmatched quote in a string argument raises L{imap4.IllegalClientResponse}. """ self.assertRaises( imap4.IllegalClientResponse, self.server.arg_astring, b'"open' ) def test_arg_astringUnmatchedLiteralBraces(self): """ An unmatched brace in a string literal's size raises L{imap4.IllegalClientResponse}. """ self.assertRaises(imap4.IllegalClientResponse, self.server.arg_astring, b"{0") def test_arg_astringInvalidLiteralSize(self): """ A non-integral string literal size raises L{imap4.IllegalClientResponse}. """ self.assertRaises( imap4.IllegalClientResponse, self.server.arg_astring, b"{[object Object]}" ) def test_arg_atomEmptyLine(self): """ An empty atom raises L{IllegalClientResponse}. """ self.assertRaises(imap4.IllegalClientResponse, self.server.arg_atom, b"") def test_arg_atomMalformedAtom(self): """ A malformed atom raises L{IllegalClientResponse}. """ self.assertRaises( imap4.IllegalClientResponse, self.server.arg_atom, b" not an atom " ) def test_arg_plistEmptyLine(self): """ An empty parenthesized list raises L{IllegalClientResponse}. """ self.assertRaises(imap4.IllegalClientResponse, self.server.arg_plist, b"") def test_arg_plistUnmatchedParentheses(self): """ A parenthesized with unmatched parentheses raises L{IllegalClientResponse}. """ self.assertRaises(imap4.IllegalClientResponse, self.server.arg_plist, b"(foo") self.assertRaises(imap4.IllegalClientResponse, self.server.arg_plist, b"foo)") def test_arg_literalEmptyLine(self): """ An empty file literal raises L{IllegalClientResponse}. """ self.assertRaises(imap4.IllegalClientResponse, self.server.arg_literal, b"") def test_arg_literalUnmatchedBraces(self): """ A literal with unmatched braces raises L{IllegalClientResponse}. """ self.assertRaises(imap4.IllegalClientResponse, self.server.arg_literal, b"{10") self.assertRaises(imap4.IllegalClientResponse, self.server.arg_literal, b"10}") def test_arg_literalInvalidLiteralSize(self): """ A non-integral literal size raises L{imap4.IllegalClientResponse}. """ self.assertRaises( imap4.IllegalClientResponse, self.server.arg_literal, b"{[object Object]}" ) def test_arg_seqsetReturnsRest(self): """ A sequence set returns the unparsed portion of a line. """ sequence = b"1:* blah blah blah" _, rest = self.server.arg_seqset(sequence) self.assertEqual(rest, b"blah blah blah") def test_arg_seqsetInvalidSequence(self): """ An invalid sequence raises L{imap4.IllegalClientResponse}. """ self.assertRaises(imap4.IllegalClientResponse, self.server.arg_seqset, b"x:y") def test_arg_flaglistOneFlag(self): """ A single flag that is not contained in a list is parsed. """ flag = b"flag" parsed, rest = self.server.arg_flaglist(flag) self.assertEqual(parsed, [flag]) self.assertFalse(rest) def test_arg_flaglistMismatchedParentehses(self): """ A list of flags with unmatched parentheses raises L{imap4.IllegalClientResponse}. """ self.assertRaises( imap4.IllegalClientResponse, self.server.arg_flaglist, b"(invalid", ) def test_arg_flaglistMalformedFlag(self): """ A list of flags that contains a malformed flag raises L{imap4.IllegalClientResponse}. """ self.assertRaises( imap4.IllegalClientResponse, self.server.arg_flaglist, b"(first \x00)" ) self.assertRaises( imap4.IllegalClientResponse, self.server.arg_flaglist, b"(first \x00second)" ) def test_opt_plistMissingOpenParenthesis(self): """ A line that does not begin with an open parenthesis (C{(}) is parsed as L{None}, and the remainder is the whole line. """ line = b"not (" plist, remainder = self.server.opt_plist(line) self.assertIsNone(plist) self.assertEqual(remainder, line) def test_opt_datetimeMissingOpenQuote(self): """ A line that does not begin with a double quote (C{"}) is parsed as L{None}, and the remainder is the whole line. """ line = b'not "' dt, remainder = self.server.opt_datetime(line) self.assertIsNone(dt) self.assertEqual(remainder, line) def test_opt_datetimeMissingCloseQuote(self): """ A line that does not have a closing double quote (C{"}) raises L{imap4.IllegalClientResponse}. """ line = b'"21-Jul-2017 19:37:07 -0700' self.assertRaises(imap4.IllegalClientResponse, self.server.opt_datetime, line) def test_opt_charsetMissingIdentifier(self): """ A line that contains C{CHARSET} but no character set identifier raises L{imap4.IllegalClientResponse}. """ line = b"CHARSET" self.assertRaises(imap4.IllegalClientResponse, self.server.opt_charset, line) def test_opt_charsetEndOfLine(self): """ A line that ends with a C{CHARSET} identifier is parsed as that identifier, and the remainder is the empty string. """ line = b"CHARSET UTF-8" identifier, remainder = self.server.opt_charset(line) self.assertEqual(identifier, b"UTF-8") self.assertEqual(remainder, b"") def test_opt_charsetWithRemainder(self): """ A line that has additional data after a C{CHARSET} identifier is parsed as that identifier, and the remainder is that additional data. """ line = b"CHARSET UTF-8 remainder" identifier, remainder = self.server.opt_charset(line) self.assertEqual(identifier, b"UTF-8") self.assertEqual(remainder, b"remainder") class IMAP4ServerSearchTests(IMAP4HelperMixin, TestCase): """ Tests for the behavior of the search_* functions in L{imap4.IMAP4Server}. """ def setUp(self): IMAP4HelperMixin.setUp(self) self.earlierQuery = ["10-Dec-2009"] self.sameDateQuery = ["13-Dec-2009"] self.laterQuery = ["16-Dec-2009"] self.seq = 0 self.msg = FakeyMessage( {"date": "Mon, 13 Dec 2009 21:25:10 GMT"}, [], "13 Dec 2009 00:00:00 GMT", "", 1234, None, ) def test_searchSentBefore(self): """ L{imap4.IMAP4Server.search_SENTBEFORE} returns True if the message date is earlier than the query date. """ self.assertFalse( self.server.search_SENTBEFORE(self.earlierQuery, self.seq, self.msg) ) self.assertTrue( self.server.search_SENTBEFORE(self.laterQuery, self.seq, self.msg) ) def test_searchWildcard(self): """ L{imap4.IMAP4Server.search_UID} returns True if the message UID is in the search range. """ self.assertFalse( self.server.search_UID([b"2:3"], self.seq, self.msg, (1, 1234)) ) # 2:* should get translated to 2:<max UID> and then to 1:2 self.assertTrue(self.server.search_UID([b"2:*"], self.seq, self.msg, (1, 1234))) self.assertTrue(self.server.search_UID([b"*"], self.seq, self.msg, (1, 1234))) def test_searchWildcardHigh(self): """ L{imap4.IMAP4Server.search_UID} should return True if there is a wildcard, because a wildcard means "highest UID in the mailbox". """ self.assertTrue( self.server.search_UID([b"1235:*"], self.seq, self.msg, (1234, 1)) ) def test_reversedSearchTerms(self): """ L{imap4.IMAP4Server.search_SENTON} returns True if the message date is the same as the query date. """ msgset = imap4.parseIdList(b"4:2") self.assertEqual(list(msgset), [2, 3, 4]) def test_searchSentOn(self): """ L{imap4.IMAP4Server.search_SENTON} returns True if the message date is the same as the query date. """ self.assertFalse( self.server.search_SENTON(self.earlierQuery, self.seq, self.msg) ) self.assertTrue( self.server.search_SENTON(self.sameDateQuery, self.seq, self.msg) ) self.assertFalse(self.server.search_SENTON(self.laterQuery, self.seq, self.msg)) def test_searchSentSince(self): """ L{imap4.IMAP4Server.search_SENTSINCE} returns True if the message date is later than the query date. """ self.assertTrue( self.server.search_SENTSINCE(self.earlierQuery, self.seq, self.msg) ) self.assertFalse( self.server.search_SENTSINCE(self.laterQuery, self.seq, self.msg) ) def test_searchOr(self): """ L{imap4.IMAP4Server.search_OR} returns true if either of the two expressions supplied to it returns true and returns false if neither does. """ self.assertTrue( self.server.search_OR( ["SENTSINCE"] + self.earlierQuery + ["SENTSINCE"] + self.laterQuery, self.seq, self.msg, (None, None), ) ) self.assertTrue( self.server.search_OR( ["SENTSINCE"] + self.laterQuery + ["SENTSINCE"] + self.earlierQuery, self.seq, self.msg, (None, None), ) ) self.assertFalse( self.server.search_OR( ["SENTON"] + self.laterQuery + ["SENTSINCE"] + self.laterQuery, self.seq, self.msg, (None, None), ) ) def test_searchNot(self): """ L{imap4.IMAP4Server.search_NOT} returns the negation of the result of the expression supplied to it. """ self.assertFalse( self.server.search_NOT( ["SENTSINCE"] + self.earlierQuery, self.seq, self.msg, (None, None) ) ) self.assertTrue( self.server.search_NOT( ["SENTON"] + self.laterQuery, self.seq, self.msg, (None, None) ) ) def test_searchBefore(self): """ L{imap4.IMAP4Server.search_BEFORE} returns True if the internal message date is before the query date. """ self.assertFalse( self.server.search_BEFORE(self.earlierQuery, self.seq, self.msg) ) self.assertFalse( self.server.search_BEFORE(self.sameDateQuery, self.seq, self.msg) ) self.assertTrue(self.server.search_BEFORE(self.laterQuery, self.seq, self.msg)) def test_searchOn(self): """ L{imap4.IMAP4Server.search_ON} returns True if the internal message date is the same as the query date. """ self.assertFalse(self.server.search_ON(self.earlierQuery, self.seq, self.msg)) self.assertFalse(self.server.search_ON(self.sameDateQuery, self.seq, self.msg)) self.assertFalse(self.server.search_ON(self.laterQuery, self.seq, self.msg)) def test_searchSince(self): """ L{imap4.IMAP4Server.search_SINCE} returns True if the internal message date is greater than the query date. """ self.assertTrue(self.server.search_SINCE(self.earlierQuery, self.seq, self.msg)) self.assertTrue( self.server.search_SINCE(self.sameDateQuery, self.seq, self.msg) ) self.assertFalse(self.server.search_SINCE(self.laterQuery, self.seq, self.msg)) @implementer(IRealm) class TestRealm: """ A L{IRealm} for tests. @cvar theAccount: An C{Account} instance. Tests can set this to ensure predictable account retrieval. """ theAccount = None def __init__(self, accountHolder=None): """ Create a realm for testing. @param accountHolder: (optional) An object whose C{theAccount} attribute will be returned instead of L{TestRealm.theAccount}. Attribute access occurs on every avatar request, so any modifications to C{accountHolder.theAccount} will be reflected here. """ if accountHolder: self._getAccount = lambda: accountHolder.theAccount else: self._getAccount = lambda: self.theAccount def requestAvatar(self, avatarId, mind, *interfaces): return imap4.IAccount, self._getAccount(), lambda: None class TestChecker: credentialInterfaces = (IUsernameHashedPassword, IUsernamePassword) users = {b"testuser": b"secret"} def requestAvatarId(self, credentials): if credentials.username in self.users: return defer.maybeDeferred( credentials.checkPassword, self.users[credentials.username] ).addCallback(self._cbCheck, credentials.username) def _cbCheck(self, result, username): if result: return username raise UnauthorizedLogin() class AuthenticatorTests(IMAP4HelperMixin, TestCase): def setUp(self): IMAP4HelperMixin.setUp(self) realm = TestRealm() realm.theAccount = Account(b"testuser") self.portal = Portal(realm) self.portal.registerChecker(TestChecker()) self.server.portal = self.portal self.authenticated = 0 self.account = realm.theAccount def test_customChallengers(self): """ L{imap4.IMAP4Server} accepts a L{dict} mapping challenge type names to L{twisted.mail.interfaces.IChallengeResponse} providers. """ @implementer(IChallengeResponse, IUsernamePassword) class SPECIALAuth: def getChallenge(self): return b"SPECIAL" def setResponse(self, response): self.username, self.password = response.split(None, 1) def moreChallenges(self): return False def checkPassword(self, password): self.password = self.password special = SPECIALAuth() verifyObject(IChallengeResponse, special) server = imap4.IMAP4Server({b"SPECIAL": SPECIALAuth}) server.portal = self.portal transport = StringTransport() server.makeConnection(transport) self.addCleanup(server.connectionLost, error.ConnectionDone("Connection done.")) self.assertIn(b"AUTH=SPECIAL", transport.value()) transport.clear() server.dataReceived(b"001 AUTHENTICATE SPECIAL\r\n") self.assertIn(base64.b64encode(special.getChallenge()), transport.value()) transport.clear() server.dataReceived(base64.b64encode(b"username password") + b"\r\n") self.assertEqual(transport.value(), b"001 OK Authentication successful\r\n") def test_unsupportedMethod(self): """ An unsupported C{AUTHENTICATE} method results in a negative response. """ server = imap4.IMAP4Server() server.portal = self.portal transport = StringTransport() server.makeConnection(transport) self.addCleanup(server.connectionLost, error.ConnectionDone("Connection done.")) transport.clear() server.dataReceived(b"001 AUTHENTICATE UNKNOWN\r\n") self.assertEqual( transport.value(), b"001 NO AUTHENTICATE method unsupported\r\n" ) def test_missingPortal(self): """ An L{imap4.IMAP4Server} that is missing a L{Portal} responds negatively to an authentication """ self.server.challengers[b"LOGIN"] = imap4.LOGINCredentials cAuth = imap4.LOGINAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) self.server.portal = None def auth(): return self.client.authenticate(b"secret") d = self.connected.addCallback(strip(auth)) d.addErrback( self.assertClientFailureMessage, b"Temporary authentication failure" ) d.addCallbacks(self._cbStopClient, self._ebGeneral) return defer.gatherResults([d, self.loopback()]) def test_challengerRaisesException(self): """ When a challenger's L{getChallenge<IChallengeResponse.getChallenge>} method raises any exception, a C{NO} response is sent. """ @implementer(IChallengeResponse) class ValueErrorAuthChallenge: message = b"A challenge failure" def getChallenge(self): raise ValueError(self.message) def setResponse(self, response): """ Never called. @param response: See L{IChallengeResponse.setResponse} """ def moreChallenges(self): """ Never called. """ @implementer(IClientAuthentication) class ValueErrorAuthenticator: def getName(self): return b"ERROR" def challengeResponse(self, secret, chal): return b"IGNORED" bad = ValueErrorAuthChallenge() verifyObject(IChallengeResponse, bad) self.server.challengers[b"ERROR"] = ValueErrorAuthChallenge self.client.registerAuthenticator(ValueErrorAuthenticator()) def auth(): return self.client.authenticate(b"secret") d = self.connected.addCallback(strip(auth)) d.addErrback( self.assertClientFailureMessage, ("Server error: " + str(ValueErrorAuthChallenge.message)).encode("ascii"), ) d.addCallbacks(self._cbStopClient, self._ebGeneral) return defer.gatherResults([d, self.loopback()]) def test_authNotBase64(self): """ A client that responds with a challenge that cannot be decoded as Base 64 receives an L{IllegalClientResponse}. """ @implementer(IChallengeResponse) class NotBase64AuthChallenge: message = b"Malformed Response - not base64" def getChallenge(self): return b"SomeChallenge" def setResponse(self, response): """ Never called. @param response: See L{IChallengeResponse.setResponse} """ def moreChallenges(self): """ Never called. """ notBase64 = NotBase64AuthChallenge() verifyObject(IChallengeResponse, notBase64) server = imap4.IMAP4Server() server.portal = self.portal server.challengers[b"NOTBASE64"] = NotBase64AuthChallenge transport = StringTransport() server.makeConnection(transport) self.addCleanup(server.connectionLost, error.ConnectionDone("Connection done.")) self.assertIn(b"AUTH=NOTBASE64", transport.value()) transport.clear() server.dataReceived(b"001 AUTHENTICATE NOTBASE64\r\n") self.assertIn(base64.b64encode(notBase64.getChallenge()), transport.value()) transport.clear() server.dataReceived(b"\x00 Not base64\r\n") self.assertEqual( transport.value(), b"".join([b"001 NO Authentication failed: ", notBase64.message, b"\r\n"]), ) def test_unhandledCredentials(self): """ A challenger that causes the login to fail L{UnhandledCredentials} results in an C{NO} response. @return: A L{Deferred} that fires when the authorization has failed. """ realm = TestRealm() portal = Portal(realm) # This portal has no checkers, so all logins will fail with # UnhandledCredentials self.server.portal = portal self.server.challengers[b"LOGIN"] = loginCred = imap4.LOGINCredentials verifyClass(IChallengeResponse, loginCred) cAuth = imap4.LOGINAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) def auth(): return self.client.authenticate(b"secret") d1 = self.connected.addCallback(strip(auth)) d1.addErrback( self.assertClientFailureMessage, b"Authentication failed: server misconfigured", ) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d def test_unexpectedLoginFailure(self): """ If the portal raises an exception other than L{UnauthorizedLogin} or L{UnhandledCredentials}, the server responds with a C{BAD} response and the exception is logged. """ class UnexpectedException(Exception): """ An unexpected exception. """ class FailingChecker: """ A credentials checker whose L{requestAvatarId} method raises L{UnexpectedException}. """ credentialInterfaces = (IUsernameHashedPassword, IUsernamePassword) def requestAvatarId(self, credentials): raise UnexpectedException("Unexpected error.") realm = TestRealm() portal = Portal(realm) portal.registerChecker(FailingChecker()) self.server.portal = portal self.server.challengers[b"LOGIN"] = loginCred = imap4.LOGINCredentials verifyClass(IChallengeResponse, loginCred) cAuth = imap4.LOGINAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) def auth(): return self.client.authenticate(b"secret") def assertUnexpectedExceptionLogged(): self.assertTrue(self.flushLoggedErrors(UnexpectedException)) d1 = self.connected.addCallback(strip(auth)) d1.addErrback( self.assertClientFailureMessage, b"Server error: login failed unexpectedly" ) d1.addCallback(strip(assertUnexpectedExceptionLogged)) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d def testCramMD5(self): self.server.challengers[b"CRAM-MD5"] = CramMD5Credentials cAuth = imap4.CramMD5ClientAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) def auth(): return self.client.authenticate(b"secret") def authed(): self.authenticated = 1 d1 = self.connected.addCallback(strip(auth)) d1.addCallbacks(strip(authed), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d2 = self.loopback() d = defer.gatherResults([d1, d2]) return d.addCallback(self._cbTestCramMD5) def _cbTestCramMD5(self, ignored): self.assertEqual(self.authenticated, 1) self.assertEqual(self.server.account, self.account) def testFailedCramMD5(self): self.server.challengers[b"CRAM-MD5"] = CramMD5Credentials cAuth = imap4.CramMD5ClientAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) def misauth(): return self.client.authenticate(b"not the secret") def authed(): self.authenticated = 1 def misauthed(): self.authenticated = -1 d1 = self.connected.addCallback(strip(misauth)) d1.addCallbacks(strip(authed), strip(misauthed)) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestFailedCramMD5) def _cbTestFailedCramMD5(self, ignored): self.assertEqual(self.authenticated, -1) self.assertEqual(self.server.account, None) def testLOGIN(self): self.server.challengers[b"LOGIN"] = loginCred = imap4.LOGINCredentials verifyClass(IChallengeResponse, loginCred) cAuth = imap4.LOGINAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) def auth(): return self.client.authenticate(b"secret") def authed(): self.authenticated = 1 d1 = self.connected.addCallback(strip(auth)) d1.addCallbacks(strip(authed), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestLOGIN) def _cbTestLOGIN(self, ignored): self.assertEqual(self.authenticated, 1) self.assertEqual(self.server.account, self.account) def testFailedLOGIN(self): self.server.challengers[b"LOGIN"] = imap4.LOGINCredentials cAuth = imap4.LOGINAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) def misauth(): return self.client.authenticate(b"not the secret") def authed(): self.authenticated = 1 def misauthed(): self.authenticated = -1 d1 = self.connected.addCallback(strip(misauth)) d1.addCallbacks(strip(authed), strip(misauthed)) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestFailedLOGIN) def _cbTestFailedLOGIN(self, ignored): self.assertEqual(self.authenticated, -1) self.assertEqual(self.server.account, None) def testPLAIN(self): self.server.challengers[b"PLAIN"] = plainCred = imap4.PLAINCredentials verifyClass(IChallengeResponse, plainCred) cAuth = imap4.PLAINAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) def auth(): return self.client.authenticate(b"secret") def authed(): self.authenticated = 1 d1 = self.connected.addCallback(strip(auth)) d1.addCallbacks(strip(authed), self._ebGeneral) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestPLAIN) def _cbTestPLAIN(self, ignored): self.assertEqual(self.authenticated, 1) self.assertEqual(self.server.account, self.account) def testFailedPLAIN(self): self.server.challengers[b"PLAIN"] = imap4.PLAINCredentials cAuth = imap4.PLAINAuthenticator(b"testuser") self.client.registerAuthenticator(cAuth) def misauth(): return self.client.authenticate(b"not the secret") def authed(): self.authenticated = 1 def misauthed(): self.authenticated = -1 d1 = self.connected.addCallback(strip(misauth)) d1.addCallbacks(strip(authed), strip(misauthed)) d1.addCallbacks(self._cbStopClient, self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestFailedPLAIN) def _cbTestFailedPLAIN(self, ignored): self.assertEqual(self.authenticated, -1) self.assertEqual(self.server.account, None) class SASLPLAINTests(TestCase): """ Tests for I{SASL PLAIN} authentication, as implemented by L{imap4.PLAINAuthenticator} and L{imap4.PLAINCredentials}. @see: U{http://www.faqs.org/rfcs/rfc2595.html} @see: U{http://www.faqs.org/rfcs/rfc4616.html} """ def test_authenticatorChallengeResponse(self): """ L{PLAINAuthenticator.challengeResponse} returns challenge strings of the form:: NUL<authn-id>NUL<secret> """ username = b"testuser" secret = b"secret" chal = b"challenge" cAuth = imap4.PLAINAuthenticator(username) response = cAuth.challengeResponse(secret, chal) self.assertEqual(response, b"\0" + username + b"\0" + secret) def test_credentialsSetResponse(self): """ L{PLAINCredentials.setResponse} parses challenge strings of the form:: NUL<authn-id>NUL<secret> """ cred = imap4.PLAINCredentials() cred.setResponse(b"\0testuser\0secret") self.assertEqual(cred.username, b"testuser") self.assertEqual(cred.password, b"secret") def test_credentialsInvalidResponse(self): """ L{PLAINCredentials.setResponse} raises L{imap4.IllegalClientResponse} when passed a string not of the expected form. """ cred = imap4.PLAINCredentials() self.assertRaises(imap4.IllegalClientResponse, cred.setResponse, b"hello") self.assertRaises( imap4.IllegalClientResponse, cred.setResponse, b"hello\0world" ) self.assertRaises( imap4.IllegalClientResponse, cred.setResponse, b"hello\0world\0Zoom!\0" ) class UnsolicitedResponseTests(IMAP4HelperMixin, TestCase): def testReadWrite(self): def login(): return self.client.login(b"testuser", b"password-test") def loggedIn(): self.server.modeChanged(1) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestReadWrite) def _cbTestReadWrite(self, ignored): E = self.client.events self.assertEqual(E, [["modeChanged", 1]]) def testReadOnly(self): def login(): return self.client.login(b"testuser", b"password-test") def loggedIn(): self.server.modeChanged(0) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestReadOnly) def _cbTestReadOnly(self, ignored): E = self.client.events self.assertEqual(E, [["modeChanged", 0]]) def testFlagChange(self): flags = {1: ["\\Answered", "\\Deleted"], 5: [], 10: ["\\Recent"]} def login(): return self.client.login(b"testuser", b"password-test") def loggedIn(): self.server.flagsChanged(flags) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestFlagChange, flags) def _cbTestFlagChange(self, ignored, flags): E = self.client.events expect = [["flagsChanged", {x[0]: x[1]}] for x in flags.items()] E.sort(key=lambda o: o[0]) expect.sort(key=lambda o: o[0]) self.assertEqual(E, expect) def testNewMessages(self): def login(): return self.client.login(b"testuser", b"password-test") def loggedIn(): self.server.newMessages(10, None) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestNewMessages) def _cbTestNewMessages(self, ignored): E = self.client.events self.assertEqual(E, [["newMessages", 10, None]]) def testNewRecentMessages(self): def login(): return self.client.login(b"testuser", b"password-test") def loggedIn(): self.server.newMessages(None, 10) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestNewRecentMessages) def _cbTestNewRecentMessages(self, ignored): E = self.client.events self.assertEqual(E, [["newMessages", None, 10]]) def testNewMessagesAndRecent(self): def login(): return self.client.login(b"testuser", b"password-test") def loggedIn(): self.server.newMessages(20, 10) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) d = defer.gatherResults([self.loopback(), d1]) return d.addCallback(self._cbTestNewMessagesAndRecent) def _cbTestNewMessagesAndRecent(self, ignored): E = self.client.events self.assertEqual(E, [["newMessages", 20, None], ["newMessages", None, 10]]) class ClientCapabilityTests(TestCase): """ Tests for issuance of the CAPABILITY command and handling of its response. """ def setUp(self): """ Create an L{imap4.IMAP4Client} connected to a L{StringTransport}. """ self.transport = StringTransport() self.protocol = imap4.IMAP4Client() self.protocol.makeConnection(self.transport) self.protocol.dataReceived(b"* OK [IMAP4rev1]\r\n") def test_simpleAtoms(self): """ A capability response consisting only of atoms without C{'='} in them should result in a dict mapping those atoms to L{None}. """ capabilitiesResult = self.protocol.getCapabilities(useCache=False) self.protocol.dataReceived(b"* CAPABILITY IMAP4rev1 LOGINDISABLED\r\n") self.protocol.dataReceived(b"0001 OK Capability completed.\r\n") def gotCapabilities(capabilities): self.assertEqual(capabilities, {b"IMAP4rev1": None, b"LOGINDISABLED": None}) capabilitiesResult.addCallback(gotCapabilities) return capabilitiesResult def test_categoryAtoms(self): """ A capability response consisting of atoms including C{'='} should have those atoms split on that byte and have capabilities in the same category aggregated into lists in the resulting dictionary. (n.b. - I made up the word "category atom"; the protocol has no notion of structure here, but rather allows each capability to define the semantics of its entry in the capability response in a freeform manner. If I had realized this earlier, the API for capabilities would look different. As it is, we can hope that no one defines any crazy semantics which are incompatible with this API, or try to figure out a better API when someone does. -exarkun) """ capabilitiesResult = self.protocol.getCapabilities(useCache=False) self.protocol.dataReceived(b"* CAPABILITY IMAP4rev1 AUTH=LOGIN AUTH=PLAIN\r\n") self.protocol.dataReceived(b"0001 OK Capability completed.\r\n") def gotCapabilities(capabilities): self.assertEqual( capabilities, {b"IMAP4rev1": None, b"AUTH": [b"LOGIN", b"PLAIN"]} ) capabilitiesResult.addCallback(gotCapabilities) return capabilitiesResult def test_mixedAtoms(self): """ A capability response consisting of both simple and category atoms of the same type should result in a list containing L{None} as well as the values for the category. """ capabilitiesResult = self.protocol.getCapabilities(useCache=False) # Exercise codepath for both orderings of =-having and =-missing # capabilities. self.protocol.dataReceived( b"* CAPABILITY IMAP4rev1 FOO FOO=BAR BAR=FOO BAR\r\n" ) self.protocol.dataReceived(b"0001 OK Capability completed.\r\n") def gotCapabilities(capabilities): self.assertEqual( capabilities, {b"IMAP4rev1": None, b"FOO": [None, b"BAR"], b"BAR": [b"FOO", None]}, ) capabilitiesResult.addCallback(gotCapabilities) return capabilitiesResult class StillSimplerClient(imap4.IMAP4Client): """ An IMAP4 client which keeps track of unsolicited flag changes. """ def __init__(self): imap4.IMAP4Client.__init__(self) self.flags = {} def flagsChanged(self, newFlags): self.flags.update(newFlags) class HandCraftedTests(IMAP4HelperMixin, TestCase): def testTrailingLiteral(self): transport = StringTransport() c = imap4.IMAP4Client() c.makeConnection(transport) c.lineReceived(b"* OK [IMAP4rev1]") def cbCheckTransport(ignored): self.assertEqual( transport.value().splitlines()[-1], b"0003 FETCH 1 (RFC822)", ) def cbSelect(ignored): d = c.fetchMessage("1") c.dataReceived( b"* 1 FETCH (RFC822 {10}\r\n0123456789\r\n RFC822.SIZE 10)\r\n" ) c.dataReceived(b"0003 OK FETCH\r\n") d.addCallback(cbCheckTransport) return d def cbLogin(ignored): d = c.select("inbox") c.lineReceived(b"0002 OK SELECT") d.addCallback(cbSelect) return d d = c.login(b"blah", b"blah") c.dataReceived(b"0001 OK LOGIN\r\n") d.addCallback(cbLogin) return d def test_fragmentedStringLiterals(self): """ String literals whose data is not immediately available are parsed. """ self.server.checker.addUser(b"testuser", b"password-test") transport = StringTransport() self.server.makeConnection(transport) transport.clear() self.server.dataReceived(b"01 LOGIN {8}\r\n") self.assertEqual(transport.value(), b"+ Ready for 8 octets of text\r\n") transport.clear() self.server.dataReceived(b"testuser {13}\r\n") self.assertEqual(transport.value(), b"+ Ready for 13 octets of text\r\n") transport.clear() self.server.dataReceived(b"password") self.assertNot(transport.value()) self.server.dataReceived(b"-test\r\n") self.assertEqual(transport.value(), b"01 OK LOGIN succeeded\r\n") self.assertEqual(self.server.state, "auth") self.server.connectionLost(error.ConnectionDone("Connection done.")) def test_emptyStringLiteral(self): """ Empty string literals are parsed. """ self.server.checker.users = {b"": b""} transport = StringTransport() self.server.makeConnection(transport) transport.clear() self.server.dataReceived(b"01 LOGIN {0}\r\n") self.assertEqual(transport.value(), b"+ Ready for 0 octets of text\r\n") transport.clear() self.server.dataReceived(b"{0}\r\n") self.assertEqual(transport.value(), b"01 OK LOGIN succeeded\r\n") self.assertEqual(self.server.state, "auth") self.server.connectionLost(error.ConnectionDone("Connection done.")) def test_unsolicitedResponseMixedWithSolicitedResponse(self): """ If unsolicited data is received along with solicited data in the response to a I{FETCH} command issued by L{IMAP4Client.fetchSpecific}, the unsolicited data is passed to the appropriate callback and not included in the result with which the L{Deferred} returned by L{IMAP4Client.fetchSpecific} fires. """ transport = StringTransport() c = StillSimplerClient() c.makeConnection(transport) c.lineReceived(b"* OK [IMAP4rev1]") def login(): d = c.login(b"blah", b"blah") c.dataReceived(b"0001 OK LOGIN\r\n") return d def select(): d = c.select("inbox") c.lineReceived(b"0002 OK SELECT") return d def fetch(): d = c.fetchSpecific( "1:*", headerType="HEADER.FIELDS", headerArgs=["SUBJECT"] ) c.dataReceived(b'* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {38}\r\n') c.dataReceived(b"Subject: Suprise for your woman...\r\n") c.dataReceived(b"\r\n") c.dataReceived(b")\r\n") c.dataReceived(b"* 1 FETCH (FLAGS (\\Seen))\r\n") c.dataReceived(b'* 2 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {75}\r\n') c.dataReceived( b"Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n" ) c.dataReceived(b"\r\n") c.dataReceived(b")\r\n") c.dataReceived(b"0003 OK FETCH completed\r\n") return d def test(res): self.assertEqual( transport.value().splitlines()[-1], b"0003 FETCH 1:* BODY[HEADER.FIELDS (SUBJECT)]", ) self.assertEqual( res, { 1: [ [ "BODY", ["HEADER.FIELDS", ["SUBJECT"]], "Subject: Suprise for your woman...\r\n\r\n", ] ], 2: [ [ "BODY", ["HEADER.FIELDS", ["SUBJECT"]], "Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n\r\n", ] ], }, ) self.assertEqual(c.flags, {1: ["\\Seen"]}) return ( login() .addCallback(strip(select)) .addCallback(strip(fetch)) .addCallback(test) ) def test_literalWithoutPrecedingWhitespace(self): """ Literals should be recognized even when they are not preceded by whitespace. """ transport = StringTransport() protocol = imap4.IMAP4Client() protocol.makeConnection(transport) protocol.lineReceived(b"* OK [IMAP4rev1]") def login(): d = protocol.login(b"blah", b"blah") protocol.dataReceived(b"0001 OK LOGIN\r\n") return d def select(): d = protocol.select(b"inbox") protocol.lineReceived(b"0002 OK SELECT") return d def fetch(): d = protocol.fetchSpecific( "1:*", headerType="HEADER.FIELDS", headerArgs=["SUBJECT"] ) protocol.dataReceived( b'* 1 FETCH (BODY[HEADER.FIELDS ({7}\r\nSUBJECT)] "Hello")\r\n' ) protocol.dataReceived(b"0003 OK FETCH completed\r\n") return d def test(result): self.assertEqual( transport.value().splitlines()[-1], b"0003 FETCH 1:* BODY[HEADER.FIELDS (SUBJECT)]", ) self.assertEqual( result, {1: [["BODY", ["HEADER.FIELDS", ["SUBJECT"]], "Hello"]]} ) d = login() d.addCallback(strip(select)) d.addCallback(strip(fetch)) d.addCallback(test) return d def test_nonIntegerLiteralLength(self): """ If the server sends a literal length which cannot be parsed as an integer, L{IMAP4Client.lineReceived} should cause the protocol to be disconnected by raising L{imap4.IllegalServerResponse}. """ transport = StringTransport() protocol = imap4.IMAP4Client() protocol.makeConnection(transport) protocol.lineReceived(b"* OK [IMAP4rev1]") def login(): d = protocol.login(b"blah", b"blah") protocol.dataReceived(b"0001 OK LOGIN\r\n") return d def select(): d = protocol.select("inbox") protocol.lineReceived(b"0002 OK SELECT") return d def fetch(): protocol.fetchSpecific( "1:*", headerType="HEADER.FIELDS", headerArgs=["SUBJECT"] ) self.assertEqual( transport.value().splitlines()[-1], b"0003 FETCH 1:* BODY[HEADER.FIELDS (SUBJECT)]", ) self.assertRaises( imap4.IllegalServerResponse, protocol.dataReceived, b"* 1 FETCH {xyz}\r\n...", ) d = login() d.addCallback(strip(select)) d.addCallback(strip(fetch)) return d def test_flagsChangedInsideFetchSpecificResponse(self): """ Any unrequested flag information received along with other requested information in an untagged I{FETCH} received in response to a request issued with L{IMAP4Client.fetchSpecific} is passed to the C{flagsChanged} callback. """ transport = StringTransport() c = StillSimplerClient() c.makeConnection(transport) c.lineReceived(b"* OK [IMAP4rev1]") def login(): d = c.login(b"blah", b"blah") c.dataReceived(b"0001 OK LOGIN\r\n") return d def select(): d = c.select("inbox") c.lineReceived(b"0002 OK SELECT") return d def fetch(): d = c.fetchSpecific( b"1:*", headerType="HEADER.FIELDS", headerArgs=["SUBJECT"] ) # This response includes FLAGS after the requested data. c.dataReceived(b'* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {22}\r\n') c.dataReceived(b"Subject: subject one\r\n") c.dataReceived(b" FLAGS (\\Recent))\r\n") # And this one includes it before! Either is possible. c.dataReceived( b'* 2 FETCH (FLAGS (\\Seen) BODY[HEADER.FIELDS ("SUBJECT")] {22}\r\n' ) c.dataReceived(b"Subject: subject two\r\n") c.dataReceived(b")\r\n") c.dataReceived(b"0003 OK FETCH completed\r\n") return d def test(res): self.assertEqual( res, { 1: [ [ "BODY", ["HEADER.FIELDS", ["SUBJECT"]], "Subject: subject one\r\n", ] ], 2: [ [ "BODY", ["HEADER.FIELDS", ["SUBJECT"]], "Subject: subject two\r\n", ] ], }, ) self.assertEqual(c.flags, {1: ["\\Recent"], 2: ["\\Seen"]}) return ( login() .addCallback(strip(select)) .addCallback(strip(fetch)) .addCallback(test) ) def test_flagsChangedInsideFetchMessageResponse(self): """ Any unrequested flag information received along with other requested information in an untagged I{FETCH} received in response to a request issued with L{IMAP4Client.fetchMessage} is passed to the C{flagsChanged} callback. """ transport = StringTransport() c = StillSimplerClient() c.makeConnection(transport) c.lineReceived(b"* OK [IMAP4rev1]") def login(): d = c.login(b"blah", b"blah") c.dataReceived(b"0001 OK LOGIN\r\n") return d def select(): d = c.select("inbox") c.lineReceived(b"0002 OK SELECT") return d def fetch(): d = c.fetchMessage("1:*") c.dataReceived(b"* 1 FETCH (RFC822 {24}\r\n") c.dataReceived(b"Subject: first subject\r\n") c.dataReceived(b" FLAGS (\\Seen))\r\n") c.dataReceived(b"* 2 FETCH (FLAGS (\\Recent \\Seen) RFC822 {25}\r\n") c.dataReceived(b"Subject: second subject\r\n") c.dataReceived(b")\r\n") c.dataReceived(b"0003 OK FETCH completed\r\n") return d def test(res): self.assertEqual( transport.value().splitlines()[-1], b"0003 FETCH 1:* (RFC822)", ) self.assertEqual( res, { 1: {"RFC822": "Subject: first subject\r\n"}, 2: {"RFC822": "Subject: second subject\r\n"}, }, ) self.assertEqual(c.flags, {1: ["\\Seen"], 2: ["\\Recent", "\\Seen"]}) return ( login() .addCallback(strip(select)) .addCallback(strip(fetch)) .addCallback(test) ) def test_authenticationChallengeDecodingException(self): """ When decoding a base64 encoded authentication message from the server, decoding errors are logged and then the client closes the connection. """ transport = StringTransportWithDisconnection() protocol = imap4.IMAP4Client() transport.protocol = protocol protocol.makeConnection(transport) protocol.lineReceived( b"* OK [CAPABILITY IMAP4rev1 IDLE NAMESPACE AUTH=CRAM-MD5] " b"Twisted IMAP4rev1 Ready" ) cAuth = imap4.CramMD5ClientAuthenticator(b"testuser") protocol.registerAuthenticator(cAuth) d = protocol.authenticate("secret") # Should really be something describing the base64 decode error. See # #6021. self.assertFailure(d, error.ConnectionDone) protocol.dataReceived(b"+ Something bad! and bad\r\n") # This should not really be logged. See #6021. logged = self.flushLoggedErrors(imap4.IllegalServerResponse) self.assertEqual(len(logged), 1) self.assertEqual(logged[0].value.args[0], b"Something bad! and bad") return d class PreauthIMAP4ClientMixin: """ Mixin for L{SynchronousTestCase} subclasses which provides a C{setUp} method which creates an L{IMAP4Client} connected to a L{StringTransport} and puts it into the I{authenticated} state. @ivar transport: A L{StringTransport} to which C{client} is connected. @ivar client: An L{IMAP4Client} which is connected to C{transport}. """ clientProtocol: Type[imap4.IMAP4Client] = imap4.IMAP4Client def setUp(self): """ Create an IMAP4Client connected to a fake transport and in the authenticated state. """ self.transport = StringTransport() self.client = self.clientProtocol() self.client.makeConnection(self.transport) self.client.dataReceived(b"* PREAUTH Hello unittest\r\n") class SelectionTestsMixin(PreauthIMAP4ClientMixin): """ Mixin for test cases which defines tests which apply to both I{EXAMINE} and I{SELECT} support. """ def _examineOrSelect(self): """ Issue either an I{EXAMINE} or I{SELECT} command (depending on C{self.method}), assert that the correct bytes are written to the transport, and return the L{Deferred} returned by whichever method was called. """ d = getattr(self.client, self.method)("foobox") self.assertEqual( self.transport.value(), b"0001 " + self.command + b" foobox\r\n" ) return d def _response(self, *lines): """ Deliver the given (unterminated) response lines to C{self.client} and then deliver a tagged SELECT or EXAMINE completion line to finish the SELECT or EXAMINE response. """ for line in lines: self.client.dataReceived(line + b"\r\n") self.client.dataReceived( b"0001 OK [READ-ONLY] " + self.command + b" completed\r\n" ) def test_exists(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{EXISTS} response, the L{Deferred} return by L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict} including the value associated with the C{'EXISTS'} key. """ d = self._examineOrSelect() self._response(b"* 3 EXISTS") self.assertEqual(self.successResultOf(d), {"READ-WRITE": False, "EXISTS": 3}) def test_nonIntegerExists(self): """ If the server returns a non-integer EXISTS value in its response to a I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fails with L{IllegalServerResponse}. """ d = self._examineOrSelect() self._response(b"* foo EXISTS") self.failureResultOf(d, imap4.IllegalServerResponse) def test_recent(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{RECENT} response, the L{Deferred} return by L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict} including the value associated with the C{'RECENT'} key. """ d = self._examineOrSelect() self._response(b"* 5 RECENT") self.assertEqual(self.successResultOf(d), {"READ-WRITE": False, "RECENT": 5}) def test_nonIntegerRecent(self): """ If the server returns a non-integer RECENT value in its response to a I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fails with L{IllegalServerResponse}. """ d = self._examineOrSelect() self._response(b"* foo RECENT") self.failureResultOf(d, imap4.IllegalServerResponse) def test_unseen(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{UNSEEN} response, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict} including the value associated with the C{'UNSEEN'} key. """ d = self._examineOrSelect() self._response(b"* OK [UNSEEN 8] Message 8 is first unseen") self.assertEqual(self.successResultOf(d), {"READ-WRITE": False, "UNSEEN": 8}) def test_nonIntegerUnseen(self): """ If the server returns a non-integer UNSEEN value in its response to a I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fails with L{IllegalServerResponse}. """ d = self._examineOrSelect() self._response(b"* OK [UNSEEN foo] Message foo is first unseen") self.failureResultOf(d, imap4.IllegalServerResponse) def test_uidvalidity(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{UIDVALIDITY} response, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict} including the value associated with the C{'UIDVALIDITY'} key. """ d = self._examineOrSelect() self._response(b"* OK [UIDVALIDITY 12345] UIDs valid") self.assertEqual( self.successResultOf(d), {"READ-WRITE": False, "UIDVALIDITY": 12345} ) def test_nonIntegerUIDVALIDITY(self): """ If the server returns a non-integer UIDVALIDITY value in its response to a I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fails with L{IllegalServerResponse}. """ d = self._examineOrSelect() self._response(b"* OK [UIDVALIDITY foo] UIDs valid") self.failureResultOf(d, imap4.IllegalServerResponse) def test_uidnext(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{UIDNEXT} response, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict} including the value associated with the C{'UIDNEXT'} key. """ d = self._examineOrSelect() self._response(b"* OK [UIDNEXT 4392] Predicted next UID") self.assertEqual( self.successResultOf(d), {"READ-WRITE": False, "UIDNEXT": 4392} ) def test_nonIntegerUIDNEXT(self): """ If the server returns a non-integer UIDNEXT value in its response to a I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fails with L{IllegalServerResponse}. """ d = self._examineOrSelect() self._response(b"* OK [UIDNEXT foo] Predicted next UID") self.failureResultOf(d, imap4.IllegalServerResponse) def test_flags(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{FLAGS} response, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict} including the value associated with the C{'FLAGS'} key. """ d = self._examineOrSelect() self._response(b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)") self.assertEqual( self.successResultOf(d), { "READ-WRITE": False, "FLAGS": ("\\Answered", "\\Flagged", "\\Deleted", "\\Seen", "\\Draft"), }, ) def test_permanentflags(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{FLAGS} response, the L{Deferred} returned by L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict} including the value associated with the C{'FLAGS'} key. """ d = self._examineOrSelect() self._response( b"* OK [PERMANENTFLAGS (\\Starred)] Just one permanent flag in " b"that list up there" ) self.assertEqual( self.successResultOf(d), {"READ-WRITE": False, "PERMANENTFLAGS": ("\\Starred",)}, ) def test_unrecognizedOk(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{OK} with unrecognized response code text, parsing does not fail. """ d = self._examineOrSelect() self._response(b"* OK [X-MADE-UP] I just made this response text up.") # The value won't show up in the result. It would be okay if it did # someday, perhaps. This shouldn't ever happen, though. self.assertEqual(self.successResultOf(d), {"READ-WRITE": False}) def test_bareOk(self): """ If the server response to a I{SELECT} or I{EXAMINE} command includes an I{OK} with no response code text, parsing does not fail. """ d = self._examineOrSelect() self._response(b"* OK") self.assertEqual(self.successResultOf(d), {"READ-WRITE": False}) class IMAP4ClientExamineTests(SelectionTestsMixin, SynchronousTestCase): """ Tests for the L{IMAP4Client.examine} method. An example of usage of the EXAMINE command from RFC 3501, section 6.3.2:: S: * 17 EXISTS S: * 2 RECENT S: * OK [UNSEEN 8] Message 8 is first unseen S: * OK [UIDVALIDITY 3857529045] UIDs valid S: * OK [UIDNEXT 4392] Predicted next UID S: * FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft) S: * OK [PERMANENTFLAGS ()] No permanent flags permitted S: A932 OK [READ-ONLY] EXAMINE completed """ method = "examine" command = b"EXAMINE" class IMAP4ClientSelectTests(SelectionTestsMixin, SynchronousTestCase): r""" Tests for the L{IMAP4Client.select} method. An example of usage of the SELECT command from RFC 3501, section 6.3.1:: C: A142 SELECT INBOX S: * 172 EXISTS S: * 1 RECENT S: * OK [UNSEEN 12] Message 12 is first unseen S: * OK [UIDVALIDITY 3857529045] UIDs valid S: * OK [UIDNEXT 4392] Predicted next UID S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited S: A142 OK [READ-WRITE] SELECT completed """ method = "select" command = b"SELECT" class IMAP4ClientExpungeTests(PreauthIMAP4ClientMixin, SynchronousTestCase): """ Tests for the L{IMAP4Client.expunge} method. An example of usage of the EXPUNGE command from RFC 3501, section 6.4.3:: C: A202 EXPUNGE S: * 3 EXPUNGE S: * 3 EXPUNGE S: * 5 EXPUNGE S: * 8 EXPUNGE S: A202 OK EXPUNGE completed """ def _expunge(self): d = self.client.expunge() self.assertEqual(self.transport.value(), b"0001 EXPUNGE\r\n") self.transport.clear() return d def _response(self, sequenceNumbers): for number in sequenceNumbers: self.client.lineReceived(networkString(f"* {number} EXPUNGE")) self.client.lineReceived(b"0001 OK EXPUNGE COMPLETED") def test_expunge(self): """ L{IMAP4Client.expunge} sends the I{EXPUNGE} command and returns a L{Deferred} which fires with a C{list} of message sequence numbers given by the server's response. """ d = self._expunge() self._response([3, 3, 5, 8]) self.assertEqual(self.successResultOf(d), [3, 3, 5, 8]) def test_nonIntegerExpunged(self): """ If the server responds with a non-integer where a message sequence number is expected, the L{Deferred} returned by L{IMAP4Client.expunge} fails with L{IllegalServerResponse}. """ d = self._expunge() self._response([3, 3, "foo", 8]) self.failureResultOf(d, imap4.IllegalServerResponse) class IMAP4ClientSearchTests(PreauthIMAP4ClientMixin, SynchronousTestCase): """ Tests for the L{IMAP4Client.search} method. An example of usage of the SEARCH command from RFC 3501, section 6.4.4:: C: A282 SEARCH FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith" S: * SEARCH 2 84 882 S: A282 OK SEARCH completed C: A283 SEARCH TEXT "string not in mailbox" S: * SEARCH S: A283 OK SEARCH completed C: A284 SEARCH CHARSET UTF-8 TEXT {6} C: XXXXXX S: * SEARCH 43 S: A284 OK SEARCH completed """ def _search(self): d = self.client.search(imap4.Query(text="ABCDEF")) self.assertEqual(self.transport.value(), b'0001 SEARCH (TEXT "ABCDEF")\r\n') return d def _response(self, messageNumbers): self.client.lineReceived( b"* SEARCH " + networkString(" ".join(map(str, messageNumbers))) ) self.client.lineReceived(b"0001 OK SEARCH completed") def test_search(self): """ L{IMAP4Client.search} sends the I{SEARCH} command and returns a L{Deferred} which fires with a C{list} of message sequence numbers given by the server's response. """ d = self._search() self._response([2, 5, 10]) self.assertEqual(self.successResultOf(d), [2, 5, 10]) def test_nonIntegerFound(self): """ If the server responds with a non-integer where a message sequence number is expected, the L{Deferred} returned by L{IMAP4Client.search} fails with L{IllegalServerResponse}. """ d = self._search() self._response([2, "foo", 10]) self.failureResultOf(d, imap4.IllegalServerResponse) class IMAP4ClientFetchTests(PreauthIMAP4ClientMixin, SynchronousTestCase): """ Tests for the L{IMAP4Client.fetch} method. See RFC 3501, section 6.4.5. """ def test_fetchUID(self): """ L{IMAP4Client.fetchUID} sends the I{FETCH UID} command and returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{dict}s mapping C{'UID'} to that message's I{UID} in the server's response. """ d = self.client.fetchUID("1:7") self.assertEqual(self.transport.value(), b"0001 FETCH 1:7 (UID)\r\n") self.client.lineReceived(b"* 2 FETCH (UID 22)") self.client.lineReceived(b"* 3 FETCH (UID 23)") self.client.lineReceived(b"* 4 FETCH (UID 24)") self.client.lineReceived(b"* 5 FETCH (UID 25)") self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual( self.successResultOf(d), {2: {"UID": "22"}, 3: {"UID": "23"}, 4: {"UID": "24"}, 5: {"UID": "25"}}, ) def test_fetchUIDNonIntegerFound(self): """ If the server responds with a non-integer where a message sequence number is expected, the L{Deferred} returned by L{IMAP4Client.fetchUID} fails with L{IllegalServerResponse}. """ d = self.client.fetchUID("1") self.assertEqual(self.transport.value(), b"0001 FETCH 1 (UID)\r\n") self.client.lineReceived(b"* foo FETCH (UID 22)") self.client.lineReceived(b"0001 OK FETCH completed") self.failureResultOf(d, imap4.IllegalServerResponse) def test_incompleteFetchUIDResponse(self): """ If the server responds with an incomplete I{FETCH} response line, the L{Deferred} returned by L{IMAP4Client.fetchUID} fails with L{IllegalServerResponse}. """ d = self.client.fetchUID("1:7") self.assertEqual(self.transport.value(), b"0001 FETCH 1:7 (UID)\r\n") self.client.lineReceived(b"* 2 FETCH (UID 22)") self.client.lineReceived(b"* 3 FETCH (UID)") self.client.lineReceived(b"* 4 FETCH (UID 24)") self.client.lineReceived(b"0001 OK FETCH completed") self.failureResultOf(d, imap4.IllegalServerResponse) def test_fetchBody(self): """ L{IMAP4Client.fetchBody} sends the I{FETCH BODY} command and returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{dict}s mapping C{'RFC822.TEXT'} to that message's body as given in the server's response. """ d = self.client.fetchBody("3") self.assertEqual(self.transport.value(), b"0001 FETCH 3 (RFC822.TEXT)\r\n") self.client.lineReceived(b'* 3 FETCH (RFC822.TEXT "Message text")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual(self.successResultOf(d), {3: {"RFC822.TEXT": "Message text"}}) def test_fetchSpecific(self): """ L{IMAP4Client.fetchSpecific} sends the I{BODY[]} command if no parameters beyond the message set to retrieve are given. It returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. """ d = self.client.fetchSpecific("7") self.assertEqual(self.transport.value(), b"0001 FETCH 7 BODY[]\r\n") self.client.lineReceived(b'* 7 FETCH (BODY[] "Some body")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual(self.successResultOf(d), {7: [["BODY", [], "Some body"]]}) def test_fetchSpecificPeek(self): """ L{IMAP4Client.fetchSpecific} issues a I{BODY.PEEK[]} command if passed C{True} for the C{peek} parameter. """ d = self.client.fetchSpecific("6", peek=True) self.assertEqual(self.transport.value(), b"0001 FETCH 6 BODY.PEEK[]\r\n") # BODY.PEEK responses are just BODY self.client.lineReceived(b'* 6 FETCH (BODY[] "Some body")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual(self.successResultOf(d), {6: [["BODY", [], "Some body"]]}) def test_fetchSpecificNumbered(self): """ L{IMAP4Client.fetchSpecific}, when passed a sequence for C{headerNumber}, sends the I{BODY[N.M]} command. It returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. """ d = self.client.fetchSpecific("7", headerNumber=(1, 2, 3)) self.assertEqual(self.transport.value(), b"0001 FETCH 7 BODY[1.2.3]\r\n") self.client.lineReceived(b'* 7 FETCH (BODY[1.2.3] "Some body")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual( self.successResultOf(d), {7: [["BODY", ["1.2.3"], "Some body"]]} ) def test_fetchSpecificText(self): """ L{IMAP4Client.fetchSpecific}, when passed C{'TEXT'} for C{headerType}, sends the I{BODY[TEXT]} command. It returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. """ d = self.client.fetchSpecific("8", headerType="TEXT") self.assertEqual(self.transport.value(), b"0001 FETCH 8 BODY[TEXT]\r\n") self.client.lineReceived(b'* 8 FETCH (BODY[TEXT] "Some body")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual( self.successResultOf(d), {8: [["BODY", ["TEXT"], "Some body"]]} ) def test_fetchSpecificNumberedText(self): """ If passed a value for the C{headerNumber} parameter and C{'TEXT'} for the C{headerType} parameter, L{IMAP4Client.fetchSpecific} sends a I{BODY[number.TEXT]} request and returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of message data given by the server's response. """ d = self.client.fetchSpecific("4", headerType="TEXT", headerNumber=7) self.assertEqual(self.transport.value(), b"0001 FETCH 4 BODY[7.TEXT]\r\n") self.client.lineReceived(b'* 4 FETCH (BODY[7.TEXT] "Some body")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual( self.successResultOf(d), {4: [["BODY", ["7.TEXT"], "Some body"]]} ) def test_incompleteFetchSpecificTextResponse(self): """ If the server responds to a I{BODY[TEXT]} request with a I{FETCH} line which is truncated after the I{BODY[TEXT]} tokens, the L{Deferred} returned by L{IMAP4Client.fetchUID} fails with L{IllegalServerResponse}. """ d = self.client.fetchSpecific("8", headerType="TEXT") self.assertEqual(self.transport.value(), b"0001 FETCH 8 BODY[TEXT]\r\n") self.client.lineReceived(b"* 8 FETCH (BODY[TEXT])") self.client.lineReceived(b"0001 OK FETCH completed") self.failureResultOf(d, imap4.IllegalServerResponse) def test_fetchSpecificMIME(self): """ L{IMAP4Client.fetchSpecific}, when passed C{'MIME'} for C{headerType}, sends the I{BODY[MIME]} command. It returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. """ d = self.client.fetchSpecific("8", headerType="MIME") self.assertEqual(self.transport.value(), b"0001 FETCH 8 BODY[MIME]\r\n") self.client.lineReceived(b'* 8 FETCH (BODY[MIME] "Some body")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual( self.successResultOf(d), {8: [["BODY", ["MIME"], "Some body"]]} ) def test_fetchSpecificPartial(self): """ L{IMAP4Client.fetchSpecific}, when passed C{offset} and C{length}, sends a partial content request (like I{BODY[TEXT]<offset.length>}). It returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. """ d = self.client.fetchSpecific("9", headerType="TEXT", offset=17, length=3) self.assertEqual(self.transport.value(), b"0001 FETCH 9 BODY[TEXT]<17.3>\r\n") self.client.lineReceived(b'* 9 FETCH (BODY[TEXT]<17> "foo")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual( self.successResultOf(d), {9: [["BODY", ["TEXT"], "<17>", "foo"]]} ) def test_incompleteFetchSpecificPartialResponse(self): """ If the server responds to a I{BODY[TEXT]} request with a I{FETCH} line which is truncated after the I{BODY[TEXT]<offset>} tokens, the L{Deferred} returned by L{IMAP4Client.fetchUID} fails with L{IllegalServerResponse}. """ d = self.client.fetchSpecific("8", headerType="TEXT") self.assertEqual(self.transport.value(), b"0001 FETCH 8 BODY[TEXT]\r\n") self.client.lineReceived(b"* 8 FETCH (BODY[TEXT]<17>)") self.client.lineReceived(b"0001 OK FETCH completed") self.failureResultOf(d, imap4.IllegalServerResponse) def test_fetchSpecificHTML(self): """ If the body of a message begins with I{<} and ends with I{>} (as, for example, HTML bodies typically will), this is still interpreted as the body by L{IMAP4Client.fetchSpecific} (and particularly, not as a length indicator for a response to a request for a partial body). """ d = self.client.fetchSpecific("7") self.assertEqual(self.transport.value(), b"0001 FETCH 7 BODY[]\r\n") self.client.lineReceived(b'* 7 FETCH (BODY[] "<html>test</html>")') self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual( self.successResultOf(d), {7: [["BODY", [], "<html>test</html>"]]} ) def assertFetchSpecificFieldsWithEmptyList(self, section): """ Assert that the provided C{BODY} section, when invoked with no arguments, produces an empty list, and that it returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. @param section: The C{BODY} section to test: either C{'HEADER.FIELDS'} or C{'HEADER.FIELDS.NOT'} @type section: L{str} """ d = self.client.fetchSpecific("10", headerType=section) self.assertEqual( self.transport.value(), b"0001 FETCH 10 BODY[" + section.encode("ascii") + b" ()]\r\n", ) # It's unclear what the response would look like - would it be # an empty string? No IMAP server parses an empty list of headers self.client.lineReceived( b"* 10 FETCH (BODY[" + section.encode("ascii") + b' ()] "")' ) self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual(self.successResultOf(d), {10: [["BODY", [section, []], ""]]}) def test_fetchSpecificHeaderFieldsWithoutHeaders(self): """ L{IMAP4Client.fetchSpecific}, when passed C{'HEADER.FIELDS'} for C{headerType} but no C{headerArgs}, sends the I{BODY[HEADER.FIELDS]} command with no arguments. It returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. """ self.assertFetchSpecificFieldsWithEmptyList("HEADER.FIELDS") def test_fetchSpecificHeaderFieldsNotWithoutHeaders(self): """ L{IMAP4Client.fetchSpecific}, when passed C{'HEADER.FIELDS.NOT'} for C{headerType} but no C{headerArgs}, sends the I{BODY[HEADER.FIELDS.NOT]} command with no arguments. It returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. """ self.assertFetchSpecificFieldsWithEmptyList("HEADER.FIELDS.NOT") def test_fetchSpecificHeader(self): """ L{IMAP4Client.fetchSpecific}, when passed C{'HEADER'} for C{headerType}, sends the I{BODY[HEADER]} command. It returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{list}s of corresponding message data given by the server's response. """ d = self.client.fetchSpecific("11", headerType="HEADER") self.assertEqual(self.transport.value(), b"0001 FETCH 11 BODY[HEADER]\r\n") self.client.lineReceived( b"* 11 FETCH (BODY[HEADER]" b' "From: someone@localhost\r\nSubject: Some subject")' ) self.client.lineReceived(b"0001 OK FETCH completed") self.assertEqual( self.successResultOf(d), { 11: [ [ "BODY", ["HEADER"], "From: someone@localhost\r\nSubject: Some subject", ] ] }, ) class IMAP4ClientStoreTests(PreauthIMAP4ClientMixin, TestCase): r""" Tests for the L{IMAP4Client.setFlags}, L{IMAP4Client.addFlags}, and L{IMAP4Client.removeFlags} methods. An example of usage of the STORE command, in terms of which these three methods are implemented, from RFC 3501, section 6.4.6:: C: A003 STORE 2:4 +FLAGS (\Deleted) S: * 2 FETCH (FLAGS (\Deleted \Seen)) S: * 3 FETCH (FLAGS (\Deleted)) S: * 4 FETCH (FLAGS (\Deleted \Flagged \Seen)) S: A003 OK STORE completed """ clientProtocol = StillSimplerClient def _flagsTest(self, method, item): """ Test a non-silent flag modifying method. Call the method, assert that the correct bytes are sent, deliver a I{FETCH} response, and assert that the result of the Deferred returned by the method is correct. @param method: The name of the method to test. @param item: The data item which is expected to be specified. """ d = getattr(self.client, method)("3", ("\\Read", "\\Seen"), False) self.assertEqual( self.transport.value(), b"0001 STORE 3 " + item + b" (\\Read \\Seen)\r\n" ) self.client.lineReceived(b"* 3 FETCH (FLAGS (\\Read \\Seen))") self.client.lineReceived(b"0001 OK STORE completed") self.assertEqual(self.successResultOf(d), {3: {"FLAGS": ["\\Read", "\\Seen"]}}) def _flagsSilentlyTest(self, method, item): """ Test a silent flag modifying method. Call the method, assert that the correct bytes are sent, deliver an I{OK} response, and assert that the result of the Deferred returned by the method is correct. @param method: The name of the method to test. @param item: The data item which is expected to be specified. """ d = getattr(self.client, method)("3", ("\\Read", "\\Seen"), True) self.assertEqual( self.transport.value(), b"0001 STORE 3 " + item + b" (\\Read \\Seen)\r\n" ) self.client.lineReceived(b"0001 OK STORE completed") self.assertEqual(self.successResultOf(d), {}) def _flagsSilentlyWithUnsolicitedDataTest(self, method, item): """ Test unsolicited data received in response to a silent flag modifying method. Call the method, assert that the correct bytes are sent, deliver the unsolicited I{FETCH} response, and assert that the result of the Deferred returned by the method is correct. @param method: The name of the method to test. @param item: The data item which is expected to be specified. """ d = getattr(self.client, method)("3", ("\\Read", "\\Seen"), True) self.assertEqual( self.transport.value(), b"0001 STORE 3 " + item + b" (\\Read \\Seen)\r\n" ) self.client.lineReceived(b"* 2 FETCH (FLAGS (\\Read \\Seen))") self.client.lineReceived(b"0001 OK STORE completed") self.assertEqual(self.successResultOf(d), {}) self.assertEqual(self.client.flags, {2: ["\\Read", "\\Seen"]}) def test_setFlags(self): """ When passed a C{False} value for the C{silent} parameter, L{IMAP4Client.setFlags} sends the I{STORE} command with a I{FLAGS} data item and returns a L{Deferred} which fires with a C{dict} mapping message sequence numbers to C{dict}s mapping C{'FLAGS'} to the new flags of those messages. """ self._flagsTest("setFlags", b"FLAGS") def test_setFlagsSilently(self): """ When passed a C{True} value for the C{silent} parameter, L{IMAP4Client.setFlags} sends the I{STORE} command with a I{FLAGS.SILENT} data item and returns a L{Deferred} which fires with an empty dictionary. """ self._flagsSilentlyTest("setFlags", b"FLAGS.SILENT") def test_setFlagsSilentlyWithUnsolicitedData(self): """ If unsolicited flag data is received in response to a I{STORE} I{FLAGS.SILENT} request, that data is passed to the C{flagsChanged} callback. """ self._flagsSilentlyWithUnsolicitedDataTest("setFlags", b"FLAGS.SILENT") def test_addFlags(self): """ L{IMAP4Client.addFlags} is like L{IMAP4Client.setFlags}, but sends I{+FLAGS} instead of I{FLAGS}. """ self._flagsTest("addFlags", b"+FLAGS") def test_addFlagsSilently(self): """ L{IMAP4Client.addFlags} with a C{True} value for C{silent} behaves like L{IMAP4Client.setFlags} with a C{True} value for C{silent}, but it sends I{+FLAGS.SILENT} instead of I{FLAGS.SILENT}. """ self._flagsSilentlyTest("addFlags", b"+FLAGS.SILENT") def test_addFlagsSilentlyWithUnsolicitedData(self): """ L{IMAP4Client.addFlags} behaves like L{IMAP4Client.setFlags} when used in silent mode and unsolicited data is received. """ self._flagsSilentlyWithUnsolicitedDataTest("addFlags", b"+FLAGS.SILENT") def test_removeFlags(self): """ L{IMAP4Client.removeFlags} is like L{IMAP4Client.setFlags}, but sends I{-FLAGS} instead of I{FLAGS}. """ self._flagsTest("removeFlags", b"-FLAGS") def test_removeFlagsSilently(self): """ L{IMAP4Client.removeFlags} with a C{True} value for C{silent} behaves like L{IMAP4Client.setFlags} with a C{True} value for C{silent}, but it sends I{-FLAGS.SILENT} instead of I{FLAGS.SILENT}. """ self._flagsSilentlyTest("removeFlags", b"-FLAGS.SILENT") def test_removeFlagsSilentlyWithUnsolicitedData(self): """ L{IMAP4Client.removeFlags} behaves like L{IMAP4Client.setFlags} when used in silent mode and unsolicited data is received. """ self._flagsSilentlyWithUnsolicitedDataTest("removeFlags", b"-FLAGS.SILENT") class IMAP4ClientStatusTests(PreauthIMAP4ClientMixin, SynchronousTestCase): """ Tests for the L{IMAP4Client.status} method. An example of usage of the STATUS command from RFC 3501, section 5.1.2:: C: A042 STATUS blurdybloop (UIDNEXT MESSAGES) S: * STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292) S: A042 OK STATUS completed @see: U{https://tools.ietf.org/html/rfc3501#section-5.1.2} """ def testUnknownName(self): """ Only allow sending the C{STATUS} names defined in RFC 3501. @see: U{https://tools.ietf.org/html/rfc3501#section-5.1.2} """ exc = self.assertRaises( ValueError, self.client.status, "ignored", "IMPOSSIBLE?!", ) self.assertEqual(str(exc), "Unknown names: " + repr({"IMPOSSIBLE?!"})) def testUndecodableName(self): """ C{STATUS} names that cannot be decoded as ASCII cause the status Deferred to fail with L{IllegalServerResponse} """ d = self.client.status("blurdybloop", "MESSAGES") self.assertEqual( self.transport.value(), b"0001 STATUS blurdybloop (MESSAGES)\r\n", ) self.client.lineReceived( b"* STATUS blurdybloop " b'(MESSAGES 1 ASCIINAME "OK" NOT\xffASCII "NO")' ) self.client.lineReceived(b"0001 OK STATUS completed") self.failureResultOf(d, imap4.IllegalServerResponse) class IMAP4ClientCopyTests(PreauthIMAP4ClientMixin, SynchronousTestCase): """ Tests for the L{IMAP4Client.copy} method. An example of the C{COPY} command, which this method implements, from RFC 3501, section 6.4.7:: C: A003 COPY 2:4 MEETING S: A003 OK COPY completed """ clientProtocol = StillSimplerClient def test_copySequenceNumbers(self): """ L{IMAP4Client.copy} copies the messages identified by their sequence numbers to the mailbox, returning a L{Deferred} that succeeds with a true value. """ d = self.client.copy("2:3", "MEETING", uid=False) self.assertEqual( self.transport.value(), b"0001 COPY 2:3 MEETING\r\n", ) self.client.lineReceived(b"0001 OK COPY completed") self.assertEqual(self.successResultOf(d), ([], b"OK COPY completed")) def test_copySequenceNumbersFails(self): """ L{IMAP4Client.copy} returns a L{Deferred} that fails with an L{IMAP4Exception} when the messages specified by the given sequence numbers could not be copied to the mailbox. """ d = self.client.copy("2:3", "MEETING", uid=False) self.assertEqual( self.transport.value(), b"0001 COPY 2:3 MEETING\r\n", ) self.client.lineReceived(b"0001 BAD COPY failed") self.assertIsInstance(self.failureResultOf(d).value, imap4.IMAP4Exception) def test_copyUIDs(self): """ L{IMAP4Client.copy} copies the messages identified by their UIDs to the mailbox, returning a L{Deferred} that succeeds with a true value. """ d = self.client.copy("2:3", "MEETING", uid=True) self.assertEqual( self.transport.value(), b"0001 UID COPY 2:3 MEETING\r\n", ) self.client.lineReceived(b"0001 OK COPY completed") self.assertEqual(self.successResultOf(d), ([], b"OK COPY completed")) def test_copyUIDsFails(self): """ L{IMAP4Client.copy} returns a L{Deferred} that fails with an L{IMAP4Exception} when the messages specified by the given UIDs could not be copied to the mailbox. """ d = self.client.copy("2:3", "MEETING", uid=True) self.assertEqual( self.transport.value(), b"0001 UID COPY 2:3 MEETING\r\n", ) self.client.lineReceived(b"0001 BAD COPY failed") self.assertIsInstance(self.failureResultOf(d).value, imap4.IMAP4Exception) class FakeyServer(imap4.IMAP4Server): state = "select" timeout = None def sendServerGreeting(self): pass @implementer(imap4.IMessage) class FakeyMessage(util.FancyStrMixin): showAttributes = ("headers", "flags", "date", "_body", "uid") def __init__(self, headers, flags, date, body, uid, subpart): self.headers = headers self.flags = flags self._body = body self.size = len(body) self.date = date self.uid = uid self.subpart = subpart def getHeaders(self, negate, *names): self.got_headers = negate, names return self.headers def getFlags(self): return self.flags def getInternalDate(self): return self.date def getBodyFile(self): return BytesIO(self._body) def getSize(self): return self.size def getUID(self): return self.uid def isMultipart(self): return self.subpart is not None def getSubPart(self, part): self.got_subpart = part return self.subpart[part] class NewStoreTests(TestCase, IMAP4HelperMixin): result = None storeArgs = None def setUp(self): self.received_messages = self.received_uid = None self.server = imap4.IMAP4Server() self.server.state = "select" self.server.mbox = self self.connected = defer.Deferred() self.client = SimpleClient(self.connected) def addListener(self, x): pass def removeListener(self, x): pass def store(self, *args, **kw): self.storeArgs = args, kw return self.response def _storeWork(self): def connected(): return self.function(self.messages, self.flags, self.silent, self.uid) def result(R): self.result = R self.connected.addCallback(strip(connected)).addCallback(result).addCallback( self._cbStopClient ).addErrback(self._ebGeneral) def check(ignored): self.assertEqual(self.result, self.expected) self.assertEqual(self.storeArgs, self.expectedArgs) d = loopback.loopbackTCP(self.server, self.client, noisy=False) d.addCallback(check) return d def testSetFlags(self, uid=0): self.function = self.client.setFlags self.messages = "1,5,9" self.flags = ["\\A", "\\B", "C"] self.silent = False self.uid = uid self.response = { 1: ["\\A", "\\B", "C"], 5: ["\\A", "\\B", "C"], 9: ["\\A", "\\B", "C"], } self.expected = { 1: {"FLAGS": ["\\A", "\\B", "C"]}, 5: {"FLAGS": ["\\A", "\\B", "C"]}, 9: {"FLAGS": ["\\A", "\\B", "C"]}, } msg = imap4.MessageSet() msg.add(1) msg.add(5) msg.add(9) self.expectedArgs = ((msg, ["\\A", "\\B", "C"], 0), {"uid": 0}) return self._storeWork() class GetBodyStructureTests(TestCase): """ Tests for L{imap4.getBodyStructure}, a helper for constructing a list which directly corresponds to the wire information needed for a I{BODY} or I{BODYSTRUCTURE} response. """ def test_singlePart(self): """ L{imap4.getBodyStructure} accepts a L{IMessagePart} provider and returns a list giving the basic fields for the I{BODY} response for that message. """ body = b"hello, world" major = "image" minor = "jpeg" charset = "us-ascii" identifier = "some kind of id" description = "great justice" encoding = "maximum" msg = FakeyMessage( { "content-type": major + "/" + minor + "; charset=" + charset + "; x=y", "content-id": identifier, "content-description": description, "content-transfer-encoding": encoding, }, (), b"", body, 123, None, ) structure = imap4.getBodyStructure(msg) self.assertEqual( [ major, minor, ["charset", charset, "x", "y"], identifier, description, encoding, len(body), ], structure, ) def test_emptyContentType(self): """ L{imap4.getBodyStructure} returns L{None} for the major and minor MIME types of a L{IMessagePart} provider whose headers lack a C{Content-Type}, or have an empty value for it. """ missing = FakeyMessage({}, (), b"", b"", 123, None) missingContentTypeStructure = imap4.getBodyStructure(missing) missingMajor, missingMinor = missingContentTypeStructure[:2] self.assertIs(None, missingMajor) self.assertIs(None, missingMinor) empty = FakeyMessage({"content-type": ""}, (), b"", b"", 123, None) emptyContentTypeStructure = imap4.getBodyStructure(empty) emptyMajor, emptyMinor = emptyContentTypeStructure[:2] self.assertIs(None, emptyMajor) self.assertIs(None, emptyMinor) newline = FakeyMessage({"content-type": "\n"}, (), b"", b"", 123, None) newlineContentTypeStructure = imap4.getBodyStructure(newline) newlineMajor, newlineMinor = newlineContentTypeStructure[:2] self.assertIs(None, newlineMajor) self.assertIs(None, newlineMinor) def test_onlyMajorContentType(self): """ L{imap4.getBodyStructure} returns only a non-L{None} major MIME type for a L{IMessagePart} provider whose headers only have a main a C{Content-Type}. """ main = FakeyMessage({"content-type": "main"}, (), b"", b"", 123, None) mainStructure = imap4.getBodyStructure(main) mainMajor, mainMinor = mainStructure[:2] self.assertEqual(mainMajor, "main") self.assertIs(mainMinor, None) def test_singlePartExtended(self): """ L{imap4.getBodyStructure} returns a list giving the basic and extended fields for a I{BODYSTRUCTURE} response if passed C{True} for the C{extended} parameter. """ body = b"hello, world" major = "image" minor = "jpeg" charset = "us-ascii" identifier = "some kind of id" description = "great justice" encoding = "maximum" md5 = "abcdefabcdef" msg = FakeyMessage( { "content-type": major + "/" + minor + "; charset=" + charset + "; x=y", "content-id": identifier, "content-description": description, "content-transfer-encoding": encoding, "content-md5": md5, "content-disposition": "attachment; name=foo; size=bar", "content-language": "fr", "content-location": "France", }, (), "", body, 123, None, ) structure = imap4.getBodyStructure(msg, extended=True) self.assertEqual( [ major, minor, ["charset", charset, "x", "y"], identifier, description, encoding, len(body), md5, ["attachment", ["name", "foo", "size", "bar"]], "fr", "France", ], structure, ) def test_singlePartWithMissing(self): """ For fields with no information contained in the message headers, L{imap4.getBodyStructure} fills in L{None} values in its result. """ major = "image" minor = "jpeg" body = b"hello, world" msg = FakeyMessage( {"content-type": major + "/" + minor}, (), b"", body, 123, None ) structure = imap4.getBodyStructure(msg, extended=True) self.assertEqual( [major, minor, None, None, None, None, len(body), None, None, None, None], structure, ) def test_textPart(self): """ For a I{text/*} message, the number of lines in the message body are included after the common single-part basic fields. """ body = b"hello, world\nhow are you?\ngoodbye\n" major = "text" minor = "jpeg" charset = "us-ascii" identifier = "some kind of id" description = "great justice" encoding = "maximum" msg = FakeyMessage( { "content-type": major + "/" + minor + "; charset=" + charset + "; x=y", "content-id": identifier, "content-description": description, "content-transfer-encoding": encoding, }, (), b"", body, 123, None, ) structure = imap4.getBodyStructure(msg) self.assertEqual( [ major, minor, ["charset", charset, "x", "y"], identifier, description, encoding, len(body), len(body.splitlines()), ], structure, ) def test_rfc822Message(self): """ For a I{message/rfc822} message, the common basic fields are followed by information about the contained message. """ body = b"hello, world\nhow are you?\ngoodbye\n" major = "text" minor = "jpeg" charset = "us-ascii" identifier = "some kind of id" description = "great justice" encoding = "maximum" msg = FakeyMessage( { "content-type": major + "/" + minor + "; charset=" + charset + "; x=y", "from": "Alice <alice@example.com>", "to": "Bob <bob@example.com>", "content-id": identifier, "content-description": description, "content-transfer-encoding": encoding, }, (), "", body, 123, None, ) container = FakeyMessage( { "content-type": "message/rfc822", }, (), b"", b"", 123, [msg], ) structure = imap4.getBodyStructure(container) self.assertEqual( [ "message", "rfc822", None, None, None, None, 0, imap4.getEnvelope(msg), imap4.getBodyStructure(msg), 3, ], structure, ) def test_multiPart(self): """ For a I{multipart/*} message, L{imap4.getBodyStructure} returns a list containing the body structure information for each part of the message followed by an element giving the MIME subtype of the message. """ oneSubPart = FakeyMessage( { "content-type": "image/jpeg; x=y", "content-id": "some kind of id", "content-description": "great justice", "content-transfer-encoding": "maximum", }, (), b"", b"hello world", 123, None, ) anotherSubPart = FakeyMessage( { "content-type": "text/plain; charset=us-ascii", }, (), b"", b"some stuff", 321, None, ) container = FakeyMessage( { "content-type": "multipart/related", }, (), b"", b"", 555, [oneSubPart, anotherSubPart], ) self.assertEqual( [ imap4.getBodyStructure(oneSubPart), imap4.getBodyStructure(anotherSubPart), "related", ], imap4.getBodyStructure(container), ) def test_multiPartExtended(self): """ When passed a I{multipart/*} message and C{True} for the C{extended} argument, L{imap4.getBodyStructure} includes extended structure information from the parts of the multipart message and extended structure information about the multipart message itself. """ oneSubPart = FakeyMessage( { b"content-type": b"image/jpeg; x=y", b"content-id": b"some kind of id", b"content-description": b"great justice", b"content-transfer-encoding": b"maximum", }, (), b"", b"hello world", 123, None, ) anotherSubPart = FakeyMessage( { b"content-type": b"text/plain; charset=us-ascii", }, (), b"", b"some stuff", 321, None, ) container = FakeyMessage( { "content-type": "multipart/related; foo=bar", "content-language": "es", "content-location": "Spain", "content-disposition": "attachment; name=monkeys", }, (), b"", b"", 555, [oneSubPart, anotherSubPart], ) self.assertEqual( [ imap4.getBodyStructure(oneSubPart, extended=True), imap4.getBodyStructure(anotherSubPart, extended=True), "related", ["foo", "bar"], ["attachment", ["name", "monkeys"]], "es", "Spain", ], imap4.getBodyStructure(container, extended=True), ) class NewFetchTests(TestCase, IMAP4HelperMixin): def setUp(self): self.received_messages = self.received_uid = None self.result = None self.server = imap4.IMAP4Server() self.server.state = "select" self.server.mbox = self self.connected = defer.Deferred() self.client = SimpleClient(self.connected) def addListener(self, x): pass def removeListener(self, x): pass def fetch(self, messages, uid): self.received_messages = messages self.received_uid = uid return iter(zip(range(len(self.msgObjs)), self.msgObjs)) def _fetchWork(self, uid): if uid: for i, msg in zip(range(len(self.msgObjs)), self.msgObjs): self.expected[i]["UID"] = str(msg.getUID()) def result(R): self.result = R self.connected.addCallback( lambda _: self.function(self.messages, uid) ).addCallback(result).addCallback(self._cbStopClient).addErrback( self._ebGeneral ) d = loopback.loopbackTCP(self.server, self.client, noisy=False) d.addCallback(lambda x: self.assertEqual(self.result, self.expected)) return d def testFetchUID(self): self.function = lambda m, u: self.client.fetchUID(m) self.messages = "7" self.msgObjs = [ FakeyMessage({}, (), b"", b"", 12345, None), FakeyMessage({}, (), b"", b"", 999, None), FakeyMessage({}, (), b"", b"", 10101, None), ] self.expected = { 0: {"UID": "12345"}, 1: {"UID": "999"}, 2: {"UID": "10101"}, } return self._fetchWork(0) def testFetchFlags(self, uid=0): self.function = self.client.fetchFlags self.messages = "9" self.msgObjs = [ FakeyMessage({}, ["FlagA", "FlagB", "\\FlagC"], b"", b"", 54321, None), FakeyMessage({}, ["\\FlagC", "FlagA", "FlagB"], b"", b"", 12345, None), ] self.expected = { 0: {"FLAGS": ["FlagA", "FlagB", "\\FlagC"]}, 1: {"FLAGS": ["\\FlagC", "FlagA", "FlagB"]}, } return self._fetchWork(uid) def testFetchFlagsUID(self): return self.testFetchFlags(1) def testFetchInternalDate(self, uid=0): self.function = self.client.fetchInternalDate self.messages = "13" self.msgObjs = [ FakeyMessage({}, (), b"Fri, 02 Nov 2003 21:25:10 GMT", b"", 23232, None), FakeyMessage({}, (), b"Thu, 29 Dec 2013 11:31:52 EST", b"", 101, None), FakeyMessage({}, (), b"Mon, 10 Mar 1992 02:44:30 CST", b"", 202, None), FakeyMessage({}, (), b"Sat, 11 Jan 2000 14:40:24 PST", b"", 303, None), ] self.expected = { 0: {"INTERNALDATE": "02-Nov-2003 21:25:10 +0000"}, 1: {"INTERNALDATE": "29-Dec-2013 11:31:52 -0500"}, 2: {"INTERNALDATE": "10-Mar-1992 02:44:30 -0600"}, 3: {"INTERNALDATE": "11-Jan-2000 14:40:24 -0800"}, } return self._fetchWork(uid) def testFetchInternalDateUID(self): return self.testFetchInternalDate(1) # if alternate locale is not available, the previous test will be skipped, # please install this locale for it to run. Avoid using locale.getlocale # to learn the current locale; its values don't round-trip well on all # platforms. Fortunately setlocale returns a value which does round-trip # well. currentLocale = locale.setlocale(locale.LC_ALL, None) try: locale.setlocale(locale.LC_ALL, "es_AR.UTF8") except locale.Error: noEsARLocale = True else: locale.setlocale(locale.LC_ALL, currentLocale) noEsARLocale = False @skipIf(noEsARLocale, "The es_AR.UTF8 locale is not installed.") def test_fetchInternalDateLocaleIndependent(self): """ The month name in the date is locale independent. """ # Fake that we're in a language where December is not Dec currentLocale = locale.setlocale(locale.LC_ALL, None) locale.setlocale(locale.LC_ALL, "es_AR.UTF8") self.addCleanup(locale.setlocale, locale.LC_ALL, currentLocale) return self.testFetchInternalDate(1) def testFetchEnvelope(self, uid=0): self.function = self.client.fetchEnvelope self.messages = "15" self.msgObjs = [ FakeyMessage( { "from": "user@domain", "to": "resu@domain", "date": "thursday", "subject": "it is a message", "message-id": "id-id-id-yayaya", }, (), b"", b"", 65656, None, ), ] self.expected = { 0: { "ENVELOPE": [ "thursday", "it is a message", [[None, None, "user", "domain"]], [[None, None, "user", "domain"]], [[None, None, "user", "domain"]], [[None, None, "resu", "domain"]], None, None, None, "id-id-id-yayaya", ] } } return self._fetchWork(uid) def testFetchEnvelopeUID(self): return self.testFetchEnvelope(1) def test_fetchBodyStructure(self, uid=0): """ L{IMAP4Client.fetchBodyStructure} issues a I{FETCH BODYSTRUCTURE} command and returns a Deferred which fires with a structure giving the result of parsing the server's response. The structure is a list reflecting the parenthesized data sent by the server, as described by RFC 3501, section 7.4.2. """ self.function = self.client.fetchBodyStructure self.messages = "3:9,10:*" self.msgObjs = [ FakeyMessage( { "content-type": 'text/plain; name=thing; key="value"', "content-id": "this-is-the-content-id", "content-description": "describing-the-content-goes-here!", "content-transfer-encoding": "8BIT", "content-md5": "abcdef123456", "content-disposition": "attachment; filename=monkeys", "content-language": "es", "content-location": "http://example.com/monkeys", }, (), "", b"Body\nText\nGoes\nHere\n", 919293, None, ) ] self.expected = { 0: { "BODYSTRUCTURE": [ "text", "plain", ["key", "value", "name", "thing"], "this-is-the-content-id", "describing-the-content-goes-here!", "8BIT", "20", "4", "abcdef123456", ["attachment", ["filename", "monkeys"]], "es", "http://example.com/monkeys", ] } } return self._fetchWork(uid) def testFetchBodyStructureUID(self): """ If passed C{True} for the C{uid} argument, C{fetchBodyStructure} can also issue a I{UID FETCH BODYSTRUCTURE} command. """ return self.test_fetchBodyStructure(1) def test_fetchBodyStructureMultipart(self, uid=0): """ L{IMAP4Client.fetchBodyStructure} can also parse the response to a I{FETCH BODYSTRUCTURE} command for a multipart message. """ self.function = self.client.fetchBodyStructure self.messages = "3:9,10:*" innerMessage = FakeyMessage( { "content-type": 'text/plain; name=thing; key="value"', "content-id": "this-is-the-content-id", "content-description": "describing-the-content-goes-here!", "content-transfer-encoding": "8BIT", "content-language": "fr", "content-md5": "123456abcdef", "content-disposition": "inline", "content-location": "outer space", }, (), b"", b"Body\nText\nGoes\nHere\n", 919293, None, ) self.msgObjs = [ FakeyMessage( { "content-type": 'multipart/mixed; boundary="xyz"', "content-language": "en", "content-location": "nearby", }, (), b"", b"", 919293, [innerMessage], ) ] self.expected = { 0: { "BODYSTRUCTURE": [ [ "text", "plain", ["key", "value", "name", "thing"], "this-is-the-content-id", "describing-the-content-goes-here!", "8BIT", "20", "4", "123456abcdef", ["inline", None], "fr", "outer space", ], "mixed", ["boundary", "xyz"], None, "en", "nearby", ] } } return self._fetchWork(uid) def testFetchSimplifiedBody(self, uid=0): self.function = self.client.fetchSimplifiedBody self.messages = "21" self.msgObjs = [ FakeyMessage( {}, (), b"", b"Yea whatever", 91825, [ FakeyMessage( {"content-type": "image/jpg"}, (), b"", b"Body Body Body", None, None, ) ], ) ] self.expected = {0: {"BODY": [None, None, None, None, None, None, "12"]}} return self._fetchWork(uid) def testFetchSimplifiedBodyUID(self): return self.testFetchSimplifiedBody(1) def testFetchSimplifiedBodyText(self, uid=0): self.function = self.client.fetchSimplifiedBody self.messages = "21" self.msgObjs = [ FakeyMessage( {"content-type": "text/plain"}, (), b"", b"Yea whatever", 91825, None ) ] self.expected = { 0: {"BODY": ["text", "plain", None, None, None, None, "12", "1"]} } return self._fetchWork(uid) def testFetchSimplifiedBodyTextUID(self): return self.testFetchSimplifiedBodyText(1) def testFetchSimplifiedBodyRFC822(self, uid=0): self.function = self.client.fetchSimplifiedBody self.messages = "21" self.msgObjs = [ FakeyMessage( {"content-type": "message/rfc822"}, (), b"", b"Yea whatever", 91825, [ FakeyMessage( {"content-type": "image/jpg"}, (), "", b"Body Body Body", None, None, ) ], ) ] self.expected = { 0: { "BODY": [ "message", "rfc822", None, None, None, None, "12", [ None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None, ], ["image", "jpg", None, None, None, None, "14"], "1", ] } } return self._fetchWork(uid) def testFetchSimplifiedBodyRFC822UID(self): return self.testFetchSimplifiedBodyRFC822(1) def test_fetchSimplifiedBodyMultipart(self): """ L{IMAP4Client.fetchSimplifiedBody} returns a dictionary mapping message sequence numbers to fetch responses for the corresponding messages. In particular, for a multipart message, the value in the dictionary maps the string C{"BODY"} to a list giving the body structure information for that message, in the form of a list of subpart body structure information followed by the subtype of the message (eg C{"alternative"} for a I{multipart/alternative} message). This structure is self-similar in the case where a subpart is itself multipart. """ self.function = self.client.fetchSimplifiedBody self.messages = "21" # A couple non-multipart messages to use as the inner-most payload singles = [ FakeyMessage( {"content-type": "text/plain"}, (), b"date", b"Stuff", 54321, None ), FakeyMessage( {"content-type": "text/html"}, (), b"date", b"Things", 32415, None ), ] # A multipart/alternative message containing the above non-multipart # messages. This will be the payload of the outer-most message. alternative = FakeyMessage( {"content-type": "multipart/alternative"}, (), b"", b"Irrelevant", 12345, singles, ) # The outer-most message, also with a multipart type, containing just # the single middle message. mixed = FakeyMessage( # The message is multipart/mixed {"content-type": "multipart/mixed"}, (), b"", b"RootOf", 98765, [alternative], ) self.msgObjs = [mixed] self.expected = { 0: { "BODY": [ [ ["text", "plain", None, None, None, None, "5", "1"], ["text", "html", None, None, None, None, "6", "1"], "alternative", ], "mixed", ] } } return self._fetchWork(False) def testFetchMessage(self, uid=0): self.function = self.client.fetchMessage self.messages = "1,3,7,10101" self.msgObjs = [ FakeyMessage({"Header": "Value"}, (), b"", b"BODY TEXT\r\n", 91, None), ] self.expected = {0: {"RFC822": "Header: Value\r\n\r\nBODY TEXT\r\n"}} return self._fetchWork(uid) def testFetchMessageUID(self): return self.testFetchMessage(1) def testFetchHeaders(self, uid=0): self.function = self.client.fetchHeaders self.messages = "9,6,2" self.msgObjs = [ FakeyMessage({"H1": "V1", "H2": "V2"}, (), b"", b"", 99, None), ] headers = nativeString(imap4._formatHeaders({"H1": "V1", "H2": "V2"})) self.expected = { 0: {"RFC822.HEADER": headers}, } return self._fetchWork(uid) def testFetchHeadersUID(self): return self.testFetchHeaders(1) def testFetchBody(self, uid=0): self.function = self.client.fetchBody self.messages = "1,2,3,4,5,6,7" self.msgObjs = [ FakeyMessage({"Header": "Value"}, (), "", b"Body goes here\r\n", 171, None), ] self.expected = { 0: {"RFC822.TEXT": "Body goes here\r\n"}, } return self._fetchWork(uid) def testFetchBodyUID(self): return self.testFetchBody(1) def testFetchBodyParts(self): """ Test the server's handling of requests for specific body sections. """ self.function = self.client.fetchSpecific self.messages = "1" outerBody = "" innerBody1 = b"Contained body message text. Squarge." innerBody2 = b"Secondary <i>message</i> text of squarge body." headers = OrderedDict() headers["from"] = "sender@host" headers["to"] = "recipient@domain" headers["subject"] = "booga booga boo" headers["content-type"] = 'multipart/alternative; boundary="xyz"' innerHeaders = OrderedDict() innerHeaders["subject"] = "this is subject text" innerHeaders["content-type"] = "text/plain" innerHeaders2 = OrderedDict() innerHeaders2["subject"] = "<b>this is subject</b>" innerHeaders2["content-type"] = "text/html" self.msgObjs = [ FakeyMessage( headers, (), None, outerBody, 123, [ FakeyMessage(innerHeaders, (), None, innerBody1, None, None), FakeyMessage(innerHeaders2, (), None, innerBody2, None, None), ], ) ] self.expected = {0: [["BODY", ["1"], "Contained body message text. Squarge."]]} def result(R): self.result = R self.connected.addCallback( lambda _: self.function(self.messages, headerNumber=1) ) self.connected.addCallback(result) self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) d = loopback.loopbackTCP(self.server, self.client, noisy=False) d.addCallback(lambda ign: self.assertEqual(self.result, self.expected)) return d def test_fetchBodyPartOfNonMultipart(self): """ Single-part messages have an implicit first part which clients should be able to retrieve explicitly. Test that a client requesting part 1 of a text/plain message receives the body of the text/plain part. """ self.function = self.client.fetchSpecific self.messages = "1" parts = [1] outerBody = b"DA body" headers = OrderedDict() headers["from"] = "sender@host" headers["to"] = "recipient@domain" headers["subject"] = "booga booga boo" headers["content-type"] = "text/plain" self.msgObjs = [FakeyMessage(headers, (), None, outerBody, 123, None)] self.expected = {0: [["BODY", ["1"], "DA body"]]} def result(R): self.result = R self.connected.addCallback( lambda _: self.function(self.messages, headerNumber=parts) ) self.connected.addCallback(result) self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) d = loopback.loopbackTCP(self.server, self.client, noisy=False) d.addCallback(lambda ign: self.assertEqual(self.result, self.expected)) return d def testFetchSize(self, uid=0): self.function = self.client.fetchSize self.messages = "1:100,2:*" self.msgObjs = [ FakeyMessage({}, (), b"", b"x" * 20, 123, None), ] self.expected = { 0: {"RFC822.SIZE": "20"}, } return self._fetchWork(uid) def testFetchSizeUID(self): return self.testFetchSize(1) def testFetchFull(self, uid=0): self.function = self.client.fetchFull self.messages = "1,3" self.msgObjs = [ FakeyMessage( {}, ("\\XYZ", "\\YZX", "Abc"), b"Sun, 25 Jul 2010 06:20:30 -0400 (EDT)", b"xyz" * 2, 654, None, ), FakeyMessage( {}, ("\\One", "\\Two", "Three"), b"Mon, 14 Apr 2003 19:43:44 -0400", b"abc" * 4, 555, None, ), ] self.expected = { 0: { "FLAGS": ["\\XYZ", "\\YZX", "Abc"], "INTERNALDATE": "25-Jul-2010 06:20:30 -0400", "RFC822.SIZE": "6", "ENVELOPE": [ None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None, ], "BODY": [None, None, None, None, None, None, "6"], }, 1: { "FLAGS": ["\\One", "\\Two", "Three"], "INTERNALDATE": "14-Apr-2003 19:43:44 -0400", "RFC822.SIZE": "12", "ENVELOPE": [ None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None, ], "BODY": [None, None, None, None, None, None, "12"], }, } return self._fetchWork(uid) def testFetchFullUID(self): return self.testFetchFull(1) def testFetchAll(self, uid=0): self.function = self.client.fetchAll self.messages = "1,2:3" self.msgObjs = [ FakeyMessage( {}, (), b"Mon, 14 Apr 2003 19:43:44 +0400", b"Lalala", 10101, None ), FakeyMessage( {}, (), b"Tue, 15 Apr 2003 19:43:44 +0200", b"Alalal", 20202, None ), ] self.expected = { 0: { "ENVELOPE": [ None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None, ], "RFC822.SIZE": "6", "INTERNALDATE": "14-Apr-2003 19:43:44 +0400", "FLAGS": [], }, 1: { "ENVELOPE": [ None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None, ], "RFC822.SIZE": "6", "INTERNALDATE": "15-Apr-2003 19:43:44 +0200", "FLAGS": [], }, } return self._fetchWork(uid) def testFetchAllUID(self): return self.testFetchAll(1) def testFetchFast(self, uid=0): self.function = self.client.fetchFast self.messages = "1" self.msgObjs = [ FakeyMessage({}, ("\\X",), b"19 Mar 2003 19:22:21 -0500", b"", 9, None), ] self.expected = { 0: { "FLAGS": ["\\X"], "INTERNALDATE": "19-Mar-2003 19:22:21 -0500", "RFC822.SIZE": "0", }, } return self._fetchWork(uid) def testFetchFastUID(self): return self.testFetchFast(1) class DefaultSearchTests(IMAP4HelperMixin, TestCase): """ Test the behavior of the server's SEARCH implementation, particularly in the face of unhandled search terms. """ def setUp(self): self.server = imap4.IMAP4Server() self.server.state = "select" self.server.mbox = self self.connected = defer.Deferred() self.client = SimpleClient(self.connected) self.msgObjs = [ FakeyMessage({}, (), b"", b"", 999, None), FakeyMessage({}, (), b"", b"", 10101, None), FakeyMessage({}, (), b"", b"", 12345, None), FakeyMessage({}, (), b"", b"", 20001, None), FakeyMessage({}, (), b"", b"", 20002, None), ] def fetch(self, messages, uid): """ Pretend to be a mailbox and let C{self.server} lookup messages on me. """ return list(zip(range(1, len(self.msgObjs) + 1), self.msgObjs)) def _messageSetSearchTest(self, queryTerms, expectedMessages): """ Issue a search with given query and verify that the returned messages match the given expected messages. @param queryTerms: A string giving the search query. @param expectedMessages: A list of the message sequence numbers expected as the result of the search. @return: A L{Deferred} which fires when the test is complete. """ def search(): return self.client.search(queryTerms) d = self.connected.addCallback(strip(search)) def searched(results): self.assertEqual(results, expectedMessages) d.addCallback(searched) d.addCallback(self._cbStopClient) d.addErrback(self._ebGeneral) self.loopback() return d def test_searchMessageSet(self): """ Test that a search which starts with a message set properly limits the search results to messages in that set. """ return self._messageSetSearchTest("1", [1]) def test_searchMessageSetWithStar(self): """ If the search filter ends with a star, all the message from the starting point are returned. """ return self._messageSetSearchTest("2:*", [2, 3, 4, 5]) def test_searchMessageSetWithStarFirst(self): """ If the search filter starts with a star, the result should be identical with if the filter would end with a star. """ return self._messageSetSearchTest("*:2", [2, 3, 4, 5]) def test_searchMessageSetUIDWithStar(self): """ If the search filter ends with a star, all the message from the starting point are returned (also for the SEARCH UID case). """ return self._messageSetSearchTest("UID 10000:*", [2, 3, 4, 5]) def test_searchMessageSetUIDWithStarFirst(self): """ If the search filter starts with a star, the result should be identical with if the filter would end with a star (also for the SEARCH UID case). """ return self._messageSetSearchTest("UID *:10000", [2, 3, 4, 5]) def test_searchMessageSetUIDWithStarAndHighStart(self): """ A search filter of 1234:* should include the UID of the last message in the mailbox, even if its UID is less than 1234. """ # in our fake mbox the highest message UID is 20002 return self._messageSetSearchTest("UID 30000:*", [5]) def test_searchMessageSetWithList(self): """ If the search filter contains nesting terms, one of which includes a message sequence set with a wildcard, IT ALL WORKS GOOD. """ # 6 is bigger than the biggest message sequence number, but that's # okay, because N:* includes the biggest message sequence number even # if N is bigger than that (read the rfc nub). return self._messageSetSearchTest("(6:*)", [5]) def test_searchOr(self): """ If the search filter contains an I{OR} term, all messages which match either subexpression are returned. """ return self._messageSetSearchTest("OR 1 2", [1, 2]) def test_searchOrMessageSet(self): """ If the search filter contains an I{OR} term with a subexpression which includes a message sequence set wildcard, all messages in that set are considered for inclusion in the results. """ return self._messageSetSearchTest("OR 2:* 2:*", [2, 3, 4, 5]) def test_searchNot(self): """ If the search filter contains a I{NOT} term, all messages which do not match the subexpression are returned. """ return self._messageSetSearchTest("NOT 3", [1, 2, 4, 5]) def test_searchNotMessageSet(self): """ If the search filter contains a I{NOT} term with a subexpression which includes a message sequence set wildcard, no messages in that set are considered for inclusion in the result. """ return self._messageSetSearchTest("NOT 2:*", [1]) def test_searchAndMessageSet(self): """ If the search filter contains multiple terms implicitly conjoined with a message sequence set wildcard, only the intersection of the results of each term are returned. """ return self._messageSetSearchTest("2:* 3", [3]) def test_searchInvalidCriteria(self): """ If the search criteria is not a valid key, a NO result is returned to the client (resulting in an error callback), and an IllegalQueryError is logged on the server side. """ queryTerms = "FOO" def search(): return self.client.search(queryTerms) d = self.connected.addCallback(strip(search)) d = self.assertFailure(d, imap4.IMAP4Exception) def errorReceived(results): """ Verify that the server logs an IllegalQueryError and the client raises an IMAP4Exception with 'Search failed:...' """ self.client.transport.loseConnection() self.server.transport.loseConnection() # Check what the server logs errors = self.flushLoggedErrors(imap4.IllegalQueryError) self.assertEqual(len(errors), 1) # Verify exception given to client has the correct message self.assertEqual( str(b"SEARCH failed: Invalid search command FOO"), str(results), ) d.addCallback(errorReceived) d.addErrback(self._ebGeneral) self.loopback() return d @implementer(imap4.ISearchableMailbox) class FetchSearchStoreTests(TestCase, IMAP4HelperMixin): def setUp(self): self.expected = self.result = None self.server_received_query = None self.server_received_uid = None self.server_received_parts = None self.server_received_messages = None self.server = imap4.IMAP4Server() self.server.state = "select" self.server.mbox = self self.connected = defer.Deferred() self.client = SimpleClient(self.connected) def search(self, query, uid): # Look for a specific bad query, so we can verify we handle it properly if query == [b"FOO"]: raise imap4.IllegalQueryError("FOO is not a valid search criteria") self.server_received_query = query self.server_received_uid = uid return self.expected def addListener(self, *a, **kw): pass removeListener = addListener def _searchWork(self, uid): def search(): return self.client.search(self.query, uid=uid) def result(R): self.result = R self.connected.addCallback(strip(search)).addCallback(result).addCallback( self._cbStopClient ).addErrback(self._ebGeneral) def check(ignored): # Ensure no short-circuiting weirdness is going on self.assertFalse(self.result is self.expected) self.assertEqual(self.result, self.expected) self.assertEqual(self.uid, self.server_received_uid) self.assertEqual( # Queries should be decoded as ASCII unless a charset # identifier is provided. See #9201. imap4.parseNestedParens(self.query.encode("charmap")), self.server_received_query, ) d = loopback.loopbackTCP(self.server, self.client, noisy=False) d.addCallback(check) return d def testSearch(self): self.query = imap4.Or( imap4.Query(header=("subject", "substring")), imap4.Query(larger=1024, smaller=4096), ) self.expected = [1, 4, 5, 7] self.uid = 0 return self._searchWork(0) def testUIDSearch(self): self.query = imap4.Or( imap4.Query(header=("subject", "substring")), imap4.Query(larger=1024, smaller=4096), ) self.uid = 1 self.expected = [1, 2, 3] return self._searchWork(1) def getUID(self, msg): try: return self.expected[msg]["UID"] except (TypeError, IndexError): return self.expected[msg - 1] except KeyError: return 42 def fetch(self, messages, uid): self.server_received_uid = uid self.server_received_messages = str(messages) return self.expected def _fetchWork(self, fetch): def result(R): self.result = R self.connected.addCallback(strip(fetch)).addCallback(result).addCallback( self._cbStopClient ).addErrback(self._ebGeneral) def check(ignored): # Ensure no short-circuiting weirdness is going on self.assertFalse(self.result is self.expected) self.parts and self.parts.sort() self.server_received_parts and self.server_received_parts.sort() if self.uid: for k, v in self.expected.items(): v["UID"] = str(k) self.assertEqual(self.result, self.expected) self.assertEqual(self.uid, self.server_received_uid) self.assertEqual(self.parts, self.server_received_parts) self.assertEqual( imap4.parseIdList(self.messages), imap4.parseIdList(self.server_received_messages), ) d = loopback.loopbackTCP(self.server, self.client, noisy=False) d.addCallback(check) return d def test_invalidTerm(self): """ If, as part of a search, an ISearchableMailbox raises an IllegalQueryError (e.g. due to invalid search criteria), client sees a failure response, and an IllegalQueryError is logged on the server. """ query = "FOO" def search(): return self.client.search(query) d = self.connected.addCallback(strip(search)) d = self.assertFailure(d, imap4.IMAP4Exception) def errorReceived(results): """ Verify that the server logs an IllegalQueryError and the client raises an IMAP4Exception with 'Search failed:...' """ self.client.transport.loseConnection() self.server.transport.loseConnection() # Check what the server logs errors = self.flushLoggedErrors(imap4.IllegalQueryError) self.assertEqual(len(errors), 1) # Verify exception given to client has the correct message self.assertEqual( str(b"SEARCH failed: FOO is not a valid search criteria"), str(results) ) d.addCallback(errorReceived) d.addErrback(self._ebGeneral) self.loopback() return d class FakeMailbox: def __init__(self): self.args = [] def addMessage(self, body, flags, date): self.args.append((body, flags, date)) return defer.succeed(None) @implementer(imap4.IMessageFile) class FeaturefulMessage: def getFlags(self): return "flags" def getInternalDate(self): return "internaldate" def open(self): return BytesIO(b"open") @implementer(imap4.IMessageCopier) class MessageCopierMailbox: def __init__(self): self.msgs = [] def copy(self, msg): self.msgs.append(msg) return len(self.msgs) class CopyWorkerTests(TestCase): def testFeaturefulMessage(self): s = imap4.IMAP4Server() # Yes. I am grabbing this uber-non-public method to test it. # It is complex. It needs to be tested directly! # Perhaps it should be refactored, simplified, or split up into # not-so-private components, but that is a task for another day. # Ha ha! Addendum! Soon it will be split up, and this test will # be re-written to just use the default adapter for IMailbox to # IMessageCopier and call .copy on that adapter. f = s._IMAP4Server__cbCopy m = FakeMailbox() d = f([(i, FeaturefulMessage()) for i in range(1, 11)], "tag", m) def cbCopy(results): for a in m.args: self.assertEqual(a[0].read(), b"open") self.assertEqual(a[1], "flags") self.assertEqual(a[2], "internaldate") for status, result in results: self.assertTrue(status) self.assertEqual(result, None) return d.addCallback(cbCopy) def testUnfeaturefulMessage(self): s = imap4.IMAP4Server() # See above comment f = s._IMAP4Server__cbCopy m = FakeMailbox() msgs = [ FakeyMessage( {"Header-Counter": str(i)}, (), b"Date", b"Body %d" % (i,), i + 10, None ) for i in range(1, 11) ] d = f([im for im in zip(range(1, 11), msgs)], "tag", m) def cbCopy(results): seen = [] for a in m.args: seen.append(a[0].read()) self.assertEqual(a[1], ()) self.assertEqual(a[2], b"Date") seen.sort() exp = sorted( b"Header-Counter: %d\r\n\r\nBody %d" % (i, i) for i in range(1, 11) ) self.assertEqual(seen, exp) for status, result in results: self.assertTrue(status) self.assertEqual(result, None) return d.addCallback(cbCopy) def testMessageCopier(self): s = imap4.IMAP4Server() # See above comment f = s._IMAP4Server__cbCopy m = MessageCopierMailbox() msgs = [object() for i in range(1, 11)] d = f([im for im in zip(range(1, 11), msgs)], b"tag", m) def cbCopy(results): self.assertEqual(results, list(zip([1] * 10, range(1, 11)))) for orig, new in zip(msgs, m.msgs): self.assertIdentical(orig, new) return d.addCallback(cbCopy) @skipIf(not ClientTLSContext, "OpenSSL not present") @skipIf(not interfaces.IReactorSSL(reactor, None), "Reactor doesn't support SSL") class TLSTests(IMAP4HelperMixin, TestCase): serverCTX = None clientCTX = None if ServerTLSContext: # type: ignore[truthy-function] serverCTX = ServerTLSContext() if ClientTLSContext: # type: ignore[truthy-function] clientCTX = ClientTLSContext() def loopback(self): return loopback.loopbackTCP(self.server, self.client, noisy=False) def testAPileOfThings(self): SimpleServer.theAccount.addMailbox(b"inbox") called = [] def login(): called.append(None) return self.client.login(b"testuser", b"password-test") def list(): called.append(None) return self.client.list(b"inbox", b"%") def status(): called.append(None) return self.client.status(b"inbox", "UIDNEXT") def examine(): called.append(None) return self.client.examine(b"inbox") def logout(): called.append(None) return self.client.logout() self.client.requireTransportSecurity = True methods = [login, list, status, examine, logout] for method in methods: self.connected.addCallback(strip(method)) self.connected.addCallbacks(self._cbStopClient, self._ebGeneral) def check(ignored): self.assertEqual(self.server.startedTLS, True) self.assertEqual(self.client.startedTLS, True) self.assertEqual(len(called), len(methods)) d = self.loopback() d.addCallback(check) return d def testLoginLogin(self): self.server.checker.addUser(b"testuser", b"password-test") success = [] self.client.registerAuthenticator(imap4.LOGINAuthenticator(b"testuser")) self.connected.addCallback( lambda _: self.client.authenticate(b"password-test") ).addCallback(lambda _: self.client.logout()).addCallback( success.append ).addCallback( self._cbStopClient ).addErrback( self._ebGeneral ) d = self.loopback() d.addCallback(lambda x: self.assertEqual(len(success), 1)) return d def startTLSAndAssertSession(self): """ Begin a C{STARTTLS} sequence and assert that it results in a TLS session. @return: A L{Deferred} that fires when the underlying connection between the client and server has been terminated. """ success = [] self.connected.addCallback(strip(self.client.startTLS)) def checkSecure(ignored): self.assertTrue(interfaces.ISSLTransport.providedBy(self.client.transport)) self.connected.addCallback(checkSecure) self.connected.addCallback(success.append) d = self.loopback() d.addCallback(lambda x: self.assertTrue(success)) return defer.gatherResults([d, self.connected]) def test_startTLS(self): """ L{IMAP4Client.startTLS} triggers TLS negotiation and returns a L{Deferred} which fires after the client's transport is using encryption. """ disconnected = self.startTLSAndAssertSession() self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) return disconnected def test_startTLSDefault(self) -> Deferred[object]: """ L{IMAPClient.startTLS} supplies a default TLS context if none is supplied. """ self.assertIsNotNone(self.client.context) self.client.context = None disconnected: Deferred[object] = self.startTLSAndAssertSession() self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) return disconnected def test_doubleSTARTTLS(self): """ A server that receives a second C{STARTTLS} sends a C{NO} response. """ class DoubleSTARTTLSClient(SimpleClient): def startTLS(self): if not self.startedTLS: return SimpleClient.startTLS(self) return self.sendCommand(imap4.Command(b"STARTTLS")) self.client = DoubleSTARTTLSClient( self.connected, contextFactory=self.clientCTX ) disconnected = self.startTLSAndAssertSession() self.connected.addCallback(strip(self.client.startTLS)) self.connected.addErrback( self.assertClientFailureMessage, b"TLS already negotiated" ) self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) return disconnected def test_startTLSWithExistingChallengers(self): """ Starting a TLS negotiation with an L{IMAP4Server} that already has C{LOGIN} and C{PLAIN} L{IChallengeResponse} factories uses those factories. """ self.server.challengers = { b"LOGIN": imap4.LOGINCredentials, b"PLAIN": imap4.PLAINCredentials, } @defer.inlineCallbacks def assertLOGINandPLAIN(): capabilities = yield self.client.getCapabilities() self.assertIn(b"AUTH", capabilities) self.assertIn(b"LOGIN", capabilities[b"AUTH"]) self.assertIn(b"PLAIN", capabilities[b"AUTH"]) self.connected.addCallback(strip(assertLOGINandPLAIN)) disconnected = self.startTLSAndAssertSession() self.connected.addCallback(strip(assertLOGINandPLAIN)) self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) return disconnected def test_loginBeforeSTARTTLS(self): """ A client that attempts to log in before issuing the C{STARTTLS} command receives a C{NO} response. """ # Prevent the client from issuing STARTTLS. self.client.startTLS = lambda: defer.succeed( ([], "OK Begin TLS negotiation now") ) self.connected.addCallback( lambda _: self.client.login(b"wrong", b"time"), ) self.connected.addErrback( self.assertClientFailureMessage, b"LOGIN is disabled before STARTTLS", ) self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) return defer.gatherResults([self.loopback(), self.connected]) def testFailedStartTLS(self): failures = [] def breakServerTLS(ign): self.server.canStartTLS = False self.connected.addCallback(breakServerTLS) self.connected.addCallback(lambda ign: self.client.startTLS()) self.connected.addErrback( lambda err: failures.append(err.trap(imap4.IMAP4Exception)) ) self.connected.addCallback(self._cbStopClient) self.connected.addErrback(self._ebGeneral) def check(ignored): self.assertTrue(failures) self.assertIdentical(failures[0], imap4.IMAP4Exception) return self.loopback().addCallback(check) class SlowMailbox(SimpleMailbox): howSlow = 2 callLater = None fetchDeferred = None # Not a very nice implementation of fetch(), but it'll # do for the purposes of testing. def fetch(self, messages, uid): d = defer.Deferred() self.callLater(self.howSlow, d.callback, ()) self.fetchDeferred.callback(None) return d class TimeoutTests(IMAP4HelperMixin, TestCase): def test_serverTimeout(self): """ The *client* has a timeout mechanism which will close connections that are inactive for a period. """ c = Clock() self.server.timeoutTest = True self.client.timeout = 5 # seconds self.client.callLater = c.callLater self.selectedArgs = None def login(): d = self.client.login(b"testuser", b"password-test") c.advance(5) d.addErrback(timedOut) return d def timedOut(failure): self._cbStopClient(None) failure.trap(error.TimeoutError) d = self.connected.addCallback(strip(login)) d.addErrback(self._ebGeneral) return defer.gatherResults([d, self.loopback()]) def test_serverTimesOut(self): """ The server times out a connection. """ c = Clock() self.server.callLater = c.callLater def login(): return self.client.login(b"testuser", b"password-test") def expireTime(): c.advance(self.server.POSTAUTH_TIMEOUT * 2) d = self.connected.addCallback(strip(login)) d.addCallback(strip(expireTime)) # The loopback method's Deferred fires the connection is # closed, and the server closes the connection as a result of # expireTime. return defer.gatherResults([d, self.loopback()]) def test_serverUnselectsMailbox(self): """ The server unsets the selected mailbox when timing out a connection. """ self.patch(SimpleServer.theAccount, "mailboxFactory", UncloseableMailbox) SimpleServer.theAccount.addMailbox("mailbox-test") mbox = SimpleServer.theAccount.mailboxes["MAILBOX-TEST"] self.assertFalse(ICloseableMailboxIMAP.providedBy(mbox)) c = Clock() self.server.callLater = c.callLater def login(): return self.client.login(b"testuser", b"password-test") def select(): return self.client.select("mailbox-test") def assertSet(): self.assertIs(mbox, self.server.mbox) def expireTime(): c.advance(self.server.POSTAUTH_TIMEOUT * 2) def assertUnset(): self.assertFalse(self.server.mbox) d = self.connected.addCallback(strip(login)) d.addCallback(strip(select)) d.addCallback(strip(assertSet)) d.addCallback(strip(expireTime)) d.addCallback(strip(assertUnset)) # The loopback method's Deferred fires the connection is # closed, and the server closes the connection as a result of # expireTime. return defer.gatherResults([d, self.loopback()]) def test_serverTimesOutAndClosesMailbox(self): """ The server closes the selected, closeable mailbox when timing out a connection. """ SimpleServer.theAccount.addMailbox("mailbox-test") mbox = SimpleServer.theAccount.mailboxes["MAILBOX-TEST"] verifyObject(ICloseableMailboxIMAP, mbox) c = Clock() self.server.callLater = c.callLater def login(): return self.client.login(b"testuser", b"password-test") def select(): return self.client.select("mailbox-test") def assertMailboxOpen(): self.assertFalse(mbox.closed) def expireTime(): c.advance(self.server.POSTAUTH_TIMEOUT * 2) def assertMailboxClosed(): self.assertTrue(mbox.closed) d = self.connected.addCallback(strip(login)) d.addCallback(strip(select)) d.addCallback(strip(assertMailboxOpen)) d.addCallback(strip(expireTime)) d.addCallback(strip(assertMailboxClosed)) # The loopback method's Deferred fires the connection is # closed, and the server closes the connection as a result of # expireTime. return defer.gatherResults([d, self.loopback()]) def test_longFetchDoesntTimeout(self): """ The connection timeout does not take effect during fetches. """ c = Clock() SlowMailbox.callLater = c.callLater SlowMailbox.fetchDeferred = defer.Deferred() self.server.callLater = c.callLater SimpleServer.theAccount.mailboxFactory = SlowMailbox SimpleServer.theAccount.addMailbox("mailbox-test") self.server.setTimeout(1) def login(): return self.client.login(b"testuser", b"password-test") def select(): self.server.setTimeout(1) return self.client.select("mailbox-test") def fetch(): return self.client.fetchUID("1:*") def stillConnected(): self.assertNotEqual(self.server.state, "timeout") def cbAdvance(ignored): for i in range(4): c.advance(0.5) SlowMailbox.fetchDeferred.addCallback(cbAdvance) d1 = self.connected.addCallback(strip(login)) d1.addCallback(strip(select)) d1.addCallback(strip(fetch)) d1.addCallback(strip(stillConnected)) d1.addCallback(self._cbStopClient) d1.addErrback(self._ebGeneral) d = defer.gatherResults([d1, self.loopback()]) return d def test_idleClientDoesDisconnect(self): """ The *server* has a timeout mechanism which will close connections that are inactive for a period. """ c = Clock() # Hook up our server protocol transport = StringTransportWithDisconnection() transport.protocol = self.server self.server.callLater = c.callLater self.server.makeConnection(transport) # Make sure we can notice when the connection goes away lost = [] connLost = self.server.connectionLost self.server.connectionLost = lambda reason: ( lost.append(None), connLost(reason), )[1] # 2/3rds of the idle timeout elapses... c.pump([0.0] + [self.server.timeOut / 3.0] * 2) self.assertFalse(lost, lost) # Now some more c.pump([0.0, self.server.timeOut / 2.0]) self.assertTrue(lost) class DisconnectionTests(TestCase): def testClientDisconnectFailsDeferreds(self): c = imap4.IMAP4Client() t = StringTransportWithDisconnection() c.makeConnection(t) d = self.assertFailure( c.login(b"testuser", "example.com"), error.ConnectionDone ) c.connectionLost(error.ConnectionDone("Connection closed")) return d class SynchronousMailbox: """ Trivial, in-memory mailbox implementation which can produce a message synchronously. """ def __init__(self, messages): self.messages = messages def fetch(self, msgset, uid): assert not uid, "Cannot handle uid requests." for msg in msgset: yield msg, self.messages[msg - 1] class PipeliningTests(TestCase): """ Tests for various aspects of the IMAP4 server's pipelining support. """ messages = [ FakeyMessage({}, [], b"", b"0", None, None), FakeyMessage({}, [], b"", b"1", None, None), FakeyMessage({}, [], b"", b"2", None, None), ] def setUp(self): self.iterators = [] self.transport = StringTransport() self.server = imap4.IMAP4Server(None, None, self.iterateInReactor) self.server.makeConnection(self.transport) mailbox = SynchronousMailbox(self.messages) # Skip over authentication and folder selection self.server.state = "select" self.server.mbox = mailbox # Get rid of any greeting junk self.transport.clear() def iterateInReactor(self, iterator): """ A fake L{imap4.iterateInReactor} that records the iterators it receives. @param iterator: An iterator. @return: A L{Deferred} associated with this iterator. """ d = defer.Deferred() self.iterators.append((iterator, d)) return d def flushPending(self, asLongAs=lambda: True): """ Advance pending iterators enqueued with L{iterateInReactor} in a round-robin fashion, resuming the transport's producer until it has completed. This ensures bodies are flushed. @param asLongAs: (optional) An optional predicate function. Flushing iterators continues as long as there are iterators and this returns L{True}. """ while self.iterators and asLongAs(): for e in self.iterators[0][0]: while self.transport.producer: self.transport.producer.resumeProducing() else: self.iterators.pop(0)[1].callback(None) def tearDown(self): self.server.connectionLost(failure.Failure(error.ConnectionDone())) def test_synchronousFetch(self): """ Test that pipelined FETCH commands which can be responded to synchronously are responded to correctly. """ # Here's some pipelined stuff self.server.dataReceived( b"01 FETCH 1 BODY[]\r\n" b"02 FETCH 2 BODY[]\r\n" b"03 FETCH 3 BODY[]\r\n" ) self.flushPending() self.assertEqual( self.transport.value(), b"".join( [ b"* 1 FETCH (BODY[] )\r\n", networkString( "01 OK FETCH completed\r\n{5}\r\n\r\n\r\n%s" % (nativeString(self.messages[0].getBodyFile().read()),) ), b"* 2 FETCH (BODY[] )\r\n", networkString( "02 OK FETCH completed\r\n{5}\r\n\r\n\r\n%s" % (nativeString(self.messages[1].getBodyFile().read()),) ), b"* 3 FETCH (BODY[] )\r\n", networkString( "03 OK FETCH completed\r\n{5}\r\n\r\n\r\n%s" % (nativeString(self.messages[2].getBodyFile().read()),) ), ] ), ) def test_bufferedServerStatus(self): """ When a server status change occurs during an ongoing FETCH command, the server status is buffered until the FETCH completes. """ self.server.dataReceived(b"01 FETCH 1,2 BODY[]\r\n") # Two iterations yields the untagged response and the first # fetched message's body twice = functools.partial(next, iter([True, True, False])) self.flushPending(asLongAs=twice) self.assertEqual( self.transport.value(), b"".join( [ # The untagged response... b"* 1 FETCH (BODY[] )\r\n", # ...and its body networkString( "{5}\r\n\r\n\r\n%s" % (nativeString(self.messages[0].getBodyFile().read()),) ), ] ), ) self.transport.clear() # A server status change... self.server.modeChanged(writeable=True) # ...remains buffered... self.assertFalse(self.transport.value()) self.flushPending() self.assertEqual( self.transport.value(), b"".join( [ # The untagged response... b"* 2 FETCH (BODY[] )\r\n", # ...the status change... b"* [READ-WRITE]\r\n", # ...and the completion status and final message's body networkString( "01 OK FETCH completed\r\n{5}\r\n\r\n\r\n%s" % (nativeString(self.messages[1].getBodyFile().read()),) ), ] ), ) class IMAP4ServerFetchTests(TestCase): """ This test case is for the FETCH tests that require a C{StringTransport}. """ def setUp(self): self.transport = StringTransport() self.server = imap4.IMAP4Server() self.server.state = "select" self.server.makeConnection(self.transport) def test_fetchWithPartialValidArgument(self): """ If by any chance, extra bytes got appended at the end of a valid FETCH arguments, the client should get a BAD - arguments invalid response. See U{RFC 3501<http://tools.ietf.org/html/rfc3501#section-6.4.5>}, section 6.4.5, """ # We need to clear out the welcome message. self.transport.clear() # Let's send out the faulty command. self.server.dataReceived(b"0001 FETCH 1 FULLL\r\n") expected = b"0001 BAD Illegal syntax: Invalid Argument\r\n" self.assertEqual(self.transport.value(), expected) self.transport.clear() self.server.connectionLost(error.ConnectionDone("Connection closed")) class LiteralTestsMixin: """ Shared tests for literal classes. @ivar literalFactory: A callable that returns instances of the literal under test. """ def setUp(self): """ Shared setup. """ self.deferred = defer.Deferred() def test_partialWrite(self): """ The literal returns L{None} when given less data than the literal requires. """ literal = self.literalFactory(1024, self.deferred) self.assertIs(None, literal.write(b"incomplete")) self.assertNoResult(self.deferred) def test_exactWrite(self): """ The literal returns an empty L{bytes} instance when given exactly the data the literal requires. """ data = b"complete" literal = self.literalFactory(len(data), self.deferred) leftover = literal.write(data) self.assertIsInstance(leftover, bytes) self.assertFalse(leftover) self.assertNoResult(self.deferred) def test_overlongWrite(self): """ The literal returns any left over L{bytes} when given more data than the literal requires. """ data = b"completeleftover" literal = self.literalFactory(len(b"complete"), self.deferred) leftover = literal.write(data) self.assertEqual(leftover, b"leftover") def test_emptyLiteral(self): """ The literal returns an empty L{bytes} instance when given an empty L{bytes} instance. """ literal = self.literalFactory(0, self.deferred) data = b"leftover" leftover = literal.write(data) self.assertEqual(leftover, data) class LiteralStringTests(LiteralTestsMixin, SynchronousTestCase): """ Tests for L{self.literalFactory}. """ literalFactory = imap4.LiteralString def test_callback(self): """ Calling L{imap4.LiteralString.callback} with a line fires the instance's L{Deferred} with a 2-L{tuple} whose first element is the collected data and whose second is the provided line. """ data = b"data" extra = b"extra" literal = imap4.LiteralString(len(data), self.deferred) for c in iterbytes(data): literal.write(c) literal.callback(b"extra") result = self.successResultOf(self.deferred) self.assertEqual(result, (data, extra)) class LiteralFileTests(LiteralTestsMixin, TestCase): """ Tests for L{imap4.LiteralFile}. """ literalFactory = imap4.LiteralFile def test_callback(self): """ Calling L{imap4.LiteralFile.callback} with a line fires the instance's L{Deferred} with a 2-L{tuple} whose first element is the file and whose second is the provided line. """ data = b"data" extra = b"extra" literal = imap4.LiteralFile(len(data), self.deferred) for c in iterbytes(data): literal.write(c) literal.callback(b"extra") result = self.successResultOf(self.deferred) self.assertEqual(len(result), 2) dataFile, extra = result self.assertEqual(dataFile.read(), b"data") def test_callbackSpooledToDisk(self): """ A L{imap4.LiteralFile} whose size exceeds the maximum in-memory size spools its content to disk, and invoking its L{callback} with a line fires the instance's L{Deferred} with a 2-L{tuple} whose first element is the spooled file and whose second is the provided line. """ data = b"data" extra = b"extra" self.patch(imap4.LiteralFile, "_memoryFileLimit", 1) literal = imap4.LiteralFile(len(data), self.deferred) for c in iterbytes(data): literal.write(c) literal.callback(b"extra") result = self.successResultOf(self.deferred) self.assertEqual(len(result), 2) dataFile, extra = result self.assertEqual(dataFile.read(), b"data") class WriteBufferTests(SynchronousTestCase): """ Tests for L{imap4.WriteBuffer}. """ def setUp(self): self.transport = StringTransport() def test_partialWrite(self): """ L{imap4.WriteBuffer} buffers writes that are smaller than its buffer size. """ buf = imap4.WriteBuffer(self.transport) data = b"x" * buf.bufferSize buf.write(data) self.assertFalse(self.transport.value()) def test_overlongWrite(self): """ L{imap4.WriteBuffer} writes data without buffering it when the size of the data exceeds the size of its buffer. """ buf = imap4.WriteBuffer(self.transport) data = b"x" * (buf.bufferSize + 1) buf.write(data) self.assertEqual(self.transport.value(), data) def test_writesImplyFlush(self): """ L{imap4.WriteBuffer} buffers writes until its buffer's size exceeds its maximum value. """ buf = imap4.WriteBuffer(self.transport) firstData = b"x" * buf.bufferSize secondData = b"y" buf.write(firstData) self.assertFalse(self.transport.value()) buf.write(secondData) self.assertEqual(self.transport.value(), firstData + secondData) def test_explicitFlush(self): """ L{imap4.WriteBuffer.flush} flushes the buffer even when its size is smaller than the buffer size. """ buf = imap4.WriteBuffer(self.transport) data = b"x" * (buf.bufferSize) buf.write(data) self.assertFalse(self.transport.value()) buf.flush() self.assertEqual(self.transport.value(), data) def test_explicitFlushEmptyBuffer(self): """ L{imap4.WriteBuffer.flush} has no effect if when the buffer is empty. """ buf = imap4.WriteBuffer(self.transport) buf.flush() self.assertFalse(self.transport.value())