From 7a36a5ae1b85c7fe2992ad553c4c55adb751a5fd Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 2 May 2025 16:49:46 +0200 Subject: [PATCH] Added tests --- modoboa/webmail/tests/__init__.py | 0 modoboa/webmail/tests/data.py | 125 ++++++++++ modoboa/webmail/tests/test_fetch_parser.py | 112 +++++++++ modoboa/webmail/tests/test_viewsets.py | 256 +++++++++++++++++++++ modoboa/webmail/viewsets.py | 8 +- 5 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 modoboa/webmail/tests/__init__.py create mode 100644 modoboa/webmail/tests/data.py create mode 100644 modoboa/webmail/tests/test_fetch_parser.py create mode 100644 modoboa/webmail/tests/test_viewsets.py diff --git a/modoboa/webmail/tests/__init__.py b/modoboa/webmail/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/webmail/tests/data.py b/modoboa/webmail/tests/data.py new file mode 100644 index 000000000..bc50591ac --- /dev/null +++ b/modoboa/webmail/tests/data.py @@ -0,0 +1,125 @@ +# flake8: noqa + +"""Tests data.""" + +BODYSTRUCTURE_SAMPLE_1 = [ + b"36 (FLAGS (\\Seen))", + b'36 (UID 36 BODYSTRUCTURE (("text" "plain" ("charset" "UTF-8") NIL NIL "QUOTED-PRINTABLE" 959 29 NIL ("inline" NIL) NIL NIL)("text" "html" ("charset" "UTF-8") NIL NIL "QUOTED-PRINTABLE" 14695 322 NIL ("inline" NIL) NIL NIL) "alternative" ("boundary" "----=_Part_2437867_661044267.1501268072105") NIL NIL NIL))', +] + +BODYSTRUCTURE_SAMPLE_2 = [ + ( + b'19 (UID 19 FLAGS (\\Seen $label4 user_flag-1) BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1" "format" "flowed") NIL NIL "7bit" 2 1 NIL NIL NIL NIL)("message" "rfc822" ("name*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74") NIL NIL "8bit" 3632 ("Wed, 13 Dec 2006 20:30:02 +0100" {70}', + b"[INSCRIPTION] R\xe9c\xe9ption de votre dossier d'inscription Free Haut D\xe9bit", + ), + ( + b' (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) ((NIL NIL "hautdebit" "freetelecom.fr")) ((NIL NIL "nguyen.antoine" "wanadoo.fr")) NIL NIL NIL "<20061213193125.9DA0919AC@dgroup2-2.proxad.net>") ("text" "plain" ("charset" "iso-8859-1") NIL NIL "8bit" 1428 38 NIL ("inline" NIL) NIL NIL) 76 NIL ("inline" ("filename*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74")) NIL NIL) "mixed" ("boundary" "------------040706080908000209030901") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {266}', + b"Date: Tue, 19 Dec 2006 19:50:13 +0100\r\nFrom: Antoine Nguyen \r\nTo: Antoine Nguyen \r\nSubject: [Fwd: [INSCRIPTION] =?ISO-8859-1?Q?R=E9c=E9ption_de_votre_?=\r\n =?ISO-8859-1?Q?dossier_d=27inscription_Free_Haut_D=E9bit=5D?=\r\n\r\n", + ), + b")", +] + +BODYSTRUCTURE_SAMPLE_3 = [ + ( + b'58 (UID 123753 BODYSTRUCTURE ((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 90 10 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1034 33 NIL NIL NIL NIL) "alternative" ("boundary" "_000_HE1PR10MB1642F2B8BA7FF8EAC0FC7AECD86E0HE1PR10MB1642EURP_") NIL NIL NIL)("application" "pdf" ("name" "=?iso-8859-1?Q?CV=5FAude=5FGIRODON=5FNGUYEN=5Fao=FBt2017_g=E9n=E9rique.pd?= =?iso-8859-1?Q?f?=") NIL {95}', + "=?iso-8859-1?Q?CV=5FAude=5FGIRODON=5FNGUYEN=5Fao=FBt2017_g=E9n=E9rique.pd?=\n =?iso-8859-1?Q?f?=", + ), + b' "base64" 94130 NIL ("attachment" ("filename" "=?iso-8859-1?Q?CV=5FAude=5FGIRODON=5FNGUYEN=5Fao=FBt2017_g=E9n=E9rique.pd?= =?iso-8859-1?Q?f?=" "size" "68787" "creation-date" "Wed, 13 Sep 2017 08:50:03 GMT" "modification-date" "Wed, 13 Sep 2017 08:50:03 GMT")) NIL NIL) "mixed" ("boundary" "_004_HE1PR10MB1642F2B8BA7FF8EAC0FC7AECD86E0HE1PR10MB1642EURP_") NIL ("fr-FR") NIL))', +] + +BODYSTRUCTURE_4 = b'BODYSTRUCTURE ((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 886 32 NIL NIL NIL NIL)("text" "html" ("charset" "us-ascii") NIL NIL "quoted-printable" 1208 16 NIL NIL NIL NIL) "alternative" ("boundary" "----=_NextPart_001_0003_01CCC564.B2F64FF0") NIL NIL NIL)("application" "octet-stream" ("name" "Carte Verte_2.pdf") NIL NIL "base64" 285610 NIL ("attachment" ("filename" "Carte Verte_2.pdf")) NIL NIL) "mixed" ("boundary" "----=_NextPart_000_0002_01CCC564.B2F64FF0") NIL NIL NIL)' + +BODYSTRUCTURE_SAMPLE_4 = [ + ( + b"855 (UID 46931 " + + BODYSTRUCTURE_4 + + b" BODY[HEADER.FIELDS (FROM TO CC DATE SUBJECT REPLY-TO MESSAGE-ID)] {235}", + b"From: \r\nTo: \r\nCc: \r\nSubject: Notre contact du 28/12/2011 - 192175092\r\nDate: Wed, 28 Dec 2011 13:29:17 +0100\r\nMessage-ID: \r\n\r\n", + ), + b")", +] + +BODYSTRUCTURE_ONLY_4 = [(b"855 (UID 46931 " + BODYSTRUCTURE_4), ")"] + +BODYSTRUCTURE_ONLY_5 = [(b"855 (UID 46932 " + BODYSTRUCTURE_4), ")"] + +BODY_PLAIN_4 = [ + (b"855 (UID 46931 BODY[1.1] {25}", b"This is a test message.\r\n"), + b")", +] + +BODYSTRUCTURE_SAMPLE_5 = [ + ( + b'856 (UID 46936 BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 724 22 NIL NIL NIL NIL)("text" "html" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 2662 48 NIL NIL NIL NIL) "alternative" ("boundary" "----=_Part_1326887_254624357.1325083973970") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {258}', + "Date: Wed, 28 Dec 2011 15:52:53 +0100 (CET)\r\nFrom: =?ISO-8859-1?Q?Malakoff_M=E9d=E9ric?= \r\nTo: Antoine Nguyen \r\nSubject: =?ISO-8859-1?Q?Votre_inscription_au_grand_Jeu_Malakoff_M=E9d=E9ric?=\r\n\r\n", + ), + b")", +] + +BODYSTRUCTURE_SAMPLE_6 = [ + ( + b'123 (UID 3 BODYSTRUCTURE (((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1266 30 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 8830 227 NIL NIL NIL NIL) "alternative" ("boundary" "_000_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL NIL NIL)("image" "png" ("name" "image005.png") "" "image005.png" "base64" 7464 NIL ("inline" ("filename" "image005.png" "size" "5453" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL)("image" "jpeg" ("name" "image006.jpg") "" "image006.jpg" "base64" 2492 NIL ("inline" ("filename" "image006.jpg" "size" "1819" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL) "related" ("boundary" "_006_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_" "type" "multipart/alternative") NIL NIL NIL)("application" "pdf" ("name" "bilan assurance CIC.PDF") NIL "bilan assurance CIC.PDF" "base64" 459532 NIL ("attachment" ("filename" "bilan assurance CIC.PDF" "size" "335811" "creation-date" "Fri, 16 Sep 2011 12:45:23 GMT" "modification-date" "Fri, 16 Sep 2011 12:45:23 GMT")) NIL NIL)(("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 1389 29 NIL NIL NIL NIL)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 1457 27 NIL NIL NIL NIL) "alternative" ("boundary" "===============0775904800==") ("inline" NIL) NIL NIL) "mixed" ("boundary" "_007_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL ("fr-FR") NIL)', + ), + b")", +] + +BODYSTRUCTURE_SAMPLE_7 = [ + ( + b'856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "(26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)', + ), + b")", +] + +BODYSTRUCTURE_SAMPLE_8 = [ + ( + b'1 (UID 947 BODYSTRUCTURE ("text" "html" ("charset" "utf-8") NIL NIL "8bit" 889 34 NIL NIL NIL NIL) BODY[HEADER.FIELDS (FROM TO CC DATE SUBJECT)] {80}', + "From: Antoine Nguyen \r\nDate: Sat, 26 Mar 2016 11:45:49 +0100\r\n\r\n", + ), + b")", +] + +BODYSTRUCTURE_SAMPLE_9 = [ + ( + b"855 (UID 46932 " + + BODYSTRUCTURE_4 + + b" BODY[HEADER.FIELDS (FROM TO CC DATE SUBJECT)] {235}", + b"From: \r\nTo: \r\nCc: \r\nSubject: Notre contact du 28/12/2011 - 192175092\r\nDate: Wed, 28 Dec 2011 13:29:17 +0100\r\nMessage-ID: \r\n\r\n", + ), + b")", +] + +BODYSTRUCTURE_SAMPLE_10 = [ + ( + b"855 (UID 46932 " + BODYSTRUCTURE_4 + b" BODY[1.1] {10})", + b"XXXXXXXX\r\n", + b"BODY[2] {10}", + b"XXXXXXXX\r\n", + b")", + ) +] + +BODYSTRUCTURE_SAMPLE_WITH_FLAGS = [ + ( + b'19 (UID 19 FLAGS (\\Seen) RFC822.SIZE 100000 BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1" "format" "flowed") NIL NIL "7bit" 2 1 NIL NIL NIL NIL)("message" "rfc822" ("name*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74") NIL NIL "8bit" 3632 ("Wed, 13 Dec 2006 20:30:02 +0100" {70}', # noqa + b"[INSCRIPTION] R\xe9c\xe9ption de votre dossier d'inscription Free Haut D\xe9bit", + ), # noqa + ( + b' (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) ((NIL NIL "hautdebit" "freetelecom.fr")) ((NIL NIL "nguyen.antoine" "wanadoo.fr")) NIL NIL NIL "<20061213193125.9DA0919AC@dgroup2-2.proxad.net>") ("text" "plain" ("charset" "iso-8859-1") NIL NIL "8bit" 1428 38 NIL ("inline" NIL) NIL NIL) 76 NIL ("inline" ("filename*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74")) NIL NIL) "mixed" ("boundary" "------------040706080908000209030901") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {266}', # noqa + b"Date: Tue, 19 Dec 2006 19:50:13 +0100\r\nFrom: Antoine Nguyen \r\nTo: Antoine Nguyen \r\nSubject: [Fwd: [INSCRIPTION] =?ISO-8859-1?Q?R=E9c=E9ption_de_votre_?=\r\n =?ISO-8859-1?Q?dossier_d=27inscription_Free_Haut_D=E9bit=5D?=\r\n\r\n", + ), + b")", +] + +BODYSTRUCTURE_EMPTY_MAIL = [ + b'33 (UID 33 BODYSTRUCTURE (("text" "plain" ("charset" "us-ascii") NIL NIL "quoted-printable" 0 0 NIL NIL NIL NIL)("application" "zip" ("name" "hotmail.com!ngyn.org!1435428000!1435514400.zip") NIL NIL "base64" 978 NIL ("attachment" ("filename" "hotmail.com!ngyn.org!1435428000!1435514400.zip")) NIL NIL) "mixed" ("boundary" "--boundary_32281_b52cf564-2a50-4f96-aeb0-ef5f83b05463") NIL NIL NIL))' +] + +EMPTY_BODY = [(b"33 (UID 33 BODY[1] {0}", b""), b")"] + +COMPLETE_MAIL = [ + ( + b"109 (UID 133872 BODY[] {2504}", + b"Return-Path: \r\nDelivered-To: test1@test.org\r\nReceived: from mail.testsrv.org\r\n\tby nelson.ngyn.org (Dovecot) with LMTP id E9jPFPHHe1u1bQAABvoInA\r\n\tfor ; Tue, 21 Aug 2018 10:06:09 +0200\r\nReceived: from [192.168.0.12] (unknown [109.9.172.169])\r\n\t(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\r\n\t(No client certificate requested)\r\n\tby mail.testsrv.org (Postfix) with ESMTPSA id 2B279E00D2\r\n\tfor ; Tue, 21 Aug 2018 10:06:09 +0200 (CEST)\r\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=test.org; s=test;\r\n\tt=1534838769; h=from:from:sender:reply-to:subject:subject:date:date:\r\n\t message-id:message-id:to:to:cc:mime-version:mime-version:\r\n\t content-type:content-type:\r\n\t content-transfer-encoding:content-transfer-encoding:in-reply-to:\r\n\t references; bh=EVfAHeUMDygbJe0SkMWJHjgXGjtiTLZnMQbyWqzsrCY=;\r\n\tb=fRc4i+WbPkbdD1BNfV9TqsZZ9nTbnHn6CKbH4nwrQnmUYQvvFUcpXC8PBURyglyLBAqPKb\r\n\tF6Dq7TnvcYdkIpR0XyMko+XB/qt8Wj0J0tC8bFxfknN8yNqj32SQAjtLrsjtzEgC1LkcLo\r\n\tcXU+FQbZR0D8EL7YD/nnsbO4mYnf3as=\r\nTo: Antoine Nguyen \r\nFrom: Antoine Nguyen \r\nSubject: Simple message\r\nMessage-ID: <7ed6e950-9e2b-b0fc-9e66-5b8358453b8d@test.org>\r\nDate: Tue, 21 Aug 2018 10:06:08 +0200\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101\r\n Thunderbird/52.9.1\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 7bit\r\nContent-Language: fr-FR\r\nARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=test.org;\r\n\ts=test; t=1534838769; h=from:from:sender:reply-to:subject:subject:date:date:\r\n\t message-id:message-id:to:to:cc:mime-version:mime-version:\r\n\t content-type:content-type:\r\n\t content-transfer-encoding:content-transfer-encoding:in-reply-to:\r\n\t references; bh=EVfAHeUMDygbJe0SkMWJHjgXGjtiTLZnMQbyWqzsrCY=;\r\n\tb=ZV1ndumHftEnhq8Z7zujDS9WUlpJmjrTIEzXsIy8+1HhZ6AEcmur5r8SmS/xfUvpoScNvF\r\n\tlYAHIdbKPX54Fpq7rZVgIhuy8vfcRGprHxH9DMhk3kHiuZLsJT1F6UdPwTYSGAWBTbg1u2\r\n\tHu2VoeSYBV5PrsINvPr1QCbo+GqTlJU=\r\nARC-Seal: i=1; s=test; d=test.org; t=1534838769; a=rsa-sha256; cv=none;\r\n\tb=CE7Q0SnkLMHx82eOtanv4pCtl3fhaPqOAYb3Nqxi5lqwDcO80vTMfGUwOv6r3Exv9Cnj3+nCxejzXtrO4TTKesL9bamOBf+veRYTLfy8wjRIfgEVXS/amOjf9u4UzGgwxL1HbhHWbCIqJhaVwrPZaHZwA9XPmboqGVQNP3i2nH0=\r\nARC-Authentication-Results: i=1; ORIGINATING;\r\n\tauth=pass smtp.auth=test1@test.org smtp.mailfrom=test1@test.org\r\nAuthentication-Results: ORIGINATING;\r\n\tauth=pass smtp.auth=test1@test.org smtp.mailfrom=test1@test.org\r\n\r\nHello!\r\n\r\n", + ), + b")", +] diff --git a/modoboa/webmail/tests/test_fetch_parser.py b/modoboa/webmail/tests/test_fetch_parser.py new file mode 100644 index 000000000..9ff075544 --- /dev/null +++ b/modoboa/webmail/tests/test_fetch_parser.py @@ -0,0 +1,112 @@ +"""FETCH parser tests.""" + +import io +import unittest + +from modoboa.webmail.lib.fetch_parser import FetchResponseParser + +from . import data + + +def dump_bodystructure(fp, bs, depth=0): + """Dump a parsed BODYSTRUCTURE.""" + indentation = " " * (depth * 4) + for mp in bs: + if isinstance(mp, list): + if isinstance(mp[0], list): + print(f"{indentation}multipart/{mp[1]}", file=fp) + dump_bodystructure(fp, mp, depth + 1) + else: + dump_bodystructure(fp, mp, depth) + elif isinstance(mp, dict): + if isinstance(mp["struct"][0], list): + print("{}multipart/{}".format(indentation, mp["struct"][1]), file=fp) + dump_bodystructure(fp, mp["struct"][0], depth + 1) + else: + print("{}{}/{}".format(indentation, *mp["struct"][:2]), file=fp) + fp.seek(0) + result = fp.read() + return result + + +class FetchParserTestCase(unittest.TestCase): + """Test FETCH parser.""" + + def setUp(self): + """Setup test env.""" + self.parser = FetchResponseParser() + + def _test_bodystructure_output(self, bs, expected): + """.""" + r = self.parser.parse(bs) + fp = io.StringIO() + output = dump_bodystructure(fp, r[list(r.keys())[0]]["BODYSTRUCTURE"]) + fp.close() + self.assertEqual(output, expected) + return r + + def test_parse_bodystructure(self): + """Test the parsing of several responses containing BS.""" + self._test_bodystructure_output( + data.BODYSTRUCTURE_SAMPLE_1, + """multipart/alternative + text/plain + text/html +""", + ) + self._test_bodystructure_output( + data.BODYSTRUCTURE_SAMPLE_2, + """multipart/mixed + text/plain + message/rfc822 +""", + ) + self._test_bodystructure_output( + data.BODYSTRUCTURE_SAMPLE_3, + """multipart/mixed + multipart/alternative + text/plain + text/html + application/pdf +""", + ) + self._test_bodystructure_output( + data.BODYSTRUCTURE_SAMPLE_4, + """multipart/mixed + multipart/alternative + text/plain + text/html + application/octet-stream +""", + ) + self._test_bodystructure_output( + data.BODYSTRUCTURE_SAMPLE_5, + """multipart/alternative + text/plain + text/html +""", + ) + self._test_bodystructure_output( + data.BODYSTRUCTURE_SAMPLE_6, + """multipart/mixed + multipart/related + multipart/alternative + text/plain + text/html + image/png + image/jpeg + application/pdf + multipart/alternative + text/plain + text/html +""", + ) + self._test_bodystructure_output( + data.BODYSTRUCTURE_SAMPLE_7, + """multipart/mixed + multipart/mixed + text/plain + application/octet-stream +""", + ) + self._test_bodystructure_output(data.BODYSTRUCTURE_SAMPLE_8, "text/html\n") diff --git a/modoboa/webmail/tests/test_viewsets.py b/modoboa/webmail/tests/test_viewsets.py new file mode 100644 index 000000000..a624504a9 --- /dev/null +++ b/modoboa/webmail/tests/test_viewsets.py @@ -0,0 +1,256 @@ +from datetime import timedelta +from io import BytesIO +import os +import shutil +import tempfile +from unittest import mock + +from django.urls import reverse +from django.utils import timezone + +from oauth2_provider.models import get_access_token_model, get_application_model + +from modoboa.core import models as core_models +from modoboa.admin import factories as admin_factories +from modoboa.lib.tests import ModoAPITestCase + +from . import data as tests_data + + +Application = get_application_model() +AccessToken = get_access_token_model() + + +def get_gif(): + """Return gif.""" + gif = BytesIO( + b"GIF87a\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00ccc,\x00" + b"\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;" + ) + gif.name = "image.gif" + return gif + + +class IMAP4Mock: + """Fake IMAP4 client.""" + + def __init__(self, *args, **kwargs): + self.untagged_responses = {} + + def _quote(self, data): + return data + + def _simple_command(self, name, *args, **kwargs): + if name == "CAPABILITY": + self.untagged_responses["CAPABILITY"] = [b""] + elif name == "LIST": + self.untagged_responses["LIST"] = [b'() "." "INBOX"'] + elif name == "NAMESPACE": + self.untagged_responses["NAMESPACE"] = [b'(("" "/")) NIL NIL'] + return "OK", None + + def append(self, *args, **kwargs): + pass + + def create(self, name): + return "OK", None + + def delete(self, name): + return "OK", None + + def list(self): + return "OK", [b'() "." "INBOX"'] + + def rename(self, oldname, newname): + return "OK", None + + def uid(self, command, *args): + if command in ["SEARCH", "SORT"]: + return "OK", [b"19"] + elif command == "FETCH": + uid = int(args[0]) + data = tests_data.BODYSTRUCTURE_SAMPLE_WITH_FLAGS + if uid == 46931: + if args[1] == "(BODYSTRUCTURE)": + data = tests_data.BODYSTRUCTURE_ONLY_4 + elif "HEADER.FIELDS" in args[1]: + data = tests_data.BODYSTRUCTURE_SAMPLE_4 + else: + data = tests_data.BODY_PLAIN_4 + elif uid == 46932: + if args[1] == "(BODYSTRUCTURE)": + data = tests_data.BODYSTRUCTURE_ONLY_5 + elif "HEADER.FIELDS" in args[1]: + data = tests_data.BODYSTRUCTURE_SAMPLE_9 + else: + data = tests_data.BODYSTRUCTURE_SAMPLE_10 + elif uid == 33: + if args[1] == "(BODYSTRUCTURE)": + data = tests_data.BODYSTRUCTURE_EMPTY_MAIL + else: + data = tests_data.EMPTY_BODY + elif uid == 133872: + data = tests_data.COMPLETE_MAIL + return "OK", data + elif command == "STORE": + return "OK", [] + + +class WebmailTestCase(ModoAPITestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + admin_factories.populate_database() + cls.user = core_models.User.objects.get(username="user@test.com") + cls.application = Application.objects.create( + name="Test Application", + redirect_uris="http://localhost", + user=cls.user, + client_type=Application.CLIENT_PUBLIC, + authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, + ) + + cls.access_token = AccessToken.objects.create( + user=cls.user, + scope="read write", + expires=timezone.now() + timedelta(seconds=300), + token="secret-access-token-key", + application=cls.application, + ) + + def setUp(self): + """Connect with a simpler user.""" + patcher = mock.patch("imaplib.IMAP4") + self.mock_imap4 = patcher.start() + self.mock_imap4.return_value = IMAP4Mock() + self.addCleanup(patcher.stop) + self.set_global_parameter("imap_port", 1435) + self.workdir = tempfile.mkdtemp() + os.mkdir(f"{self.workdir}/webmail") + self.set_global_parameter("update_scheme", False, app="core") + + def tearDown(self): + """Cleanup.""" + shutil.rmtree(self.workdir) + + def authenticate(self, username: str = "user@test.com"): + self.client.credentials(Authorization="Bearer " + self.access_token.token) + + +class UserMailboxViewSetTestCase(WebmailTestCase): + + def test_list(self): + self.authenticate() + url = reverse("v2:webmail-mailbox-list") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()["mailboxes"]), 5) + + def test_quota(self): + self.authenticate() + url = reverse("v2:webmail-mailbox-quota") + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + response = self.client.get(f"{url}?mailbox=INBOX") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["usage"], -1) + + def test_create(self): + self.authenticate() + url = reverse("v2:webmail-mailbox-list") + body = {"name": "Test"} + response = self.client.post(url, body, format="json") + self.assertEqual(response.status_code, 201) + body = {"parent": "Test", "name": "Test"} + response = self.client.post(url, body, format="json") + self.assertEqual(response.status_code, 201) + + def test_rename(self): + self.authenticate() + url = reverse("v2:webmail-mailbox-rename") + body = {"name": "Test renamed", "oldname": "Test"} + response = self.client.post(url, body, format="json") + self.assertEqual(response.status_code, 200) + + def test_compress(self): + self.authenticate() + url = reverse("v2:webmail-mailbox-compress") + body = {"name": "INBOX"} + response = self.client.post(url, body, format="json") + self.assertEqual(response.status_code, 204) + + def test_empty(self): + self.authenticate() + url = reverse("v2:webmail-mailbox-empty") + body = {"name": "INBOX"} + response = self.client.post(url, body, format="json") + self.assertEqual(response.status_code, 404) + body = {"name": "Trash"} + response = self.client.post(url, body, format="json") + self.assertEqual(response.status_code, 204) + + def test_delete(self): + self.authenticate() + url = reverse("v2:webmail-mailbox-delete") + body = {"name": "Test"} + response = self.client.post(url, body, format="json") + self.assertEqual(response.status_code, 204) + + +class UserEmailViewSetTestCase(WebmailTestCase): + + def test_listmailbox(self): + """Check listmailbox action.""" + self.authenticate() + url = reverse("v2:webmail-email-list") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + content = response.json() + self.assertEqual(content["count"], 1) + self.assertEqual( + content["results"][0]["from_address"]["address"], + "nguyen.antoine@wanadoo.fr", + ) + + response = self.client.get(f"{url}?search=Réception") + self.assertEqual(response.status_code, 200) + self.assertEqual( + content["results"][0]["from_address"]["address"], + "nguyen.antoine@wanadoo.fr", + ) + + def test_getmailsource(self): + """Try to display a message's source.""" + self.authenticate() + url = reverse("v2:webmail-email-source") + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + response = self.client.get(f"{url}?mailbox=INBOX&mailid=133872") + self.assertEqual(response.status_code, 200) + self.assertIn("Message-ID", response.json()["source"]) + + +class ComposeSessionViewSetTestCase(WebmailTestCase): + + def test_create(self): + self.authenticate() + url = reverse("v2:webmail-compose-session-list") + response = self.client.post(url) + self.assertEqual(response.status_code, 201) + self.assertIn("uid", response.json()) + + def test_get_allowed_senders(self): + self.authenticate() + url = reverse("v2:webmail-compose-session-allowed-senders") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 2) + + def test_attachments(self): + self.authenticate() + url = reverse("v2:webmail-compose-session-list") + response = self.client.post(url) + uid = response.json()["uid"] + url = reverse("v2:webmail-compose-session-attachments", args=[uid]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) diff --git a/modoboa/webmail/viewsets.py b/modoboa/webmail/viewsets.py index a36fbe38d..17a2f2e0a 100644 --- a/modoboa/webmail/viewsets.py +++ b/modoboa/webmail/viewsets.py @@ -9,12 +9,14 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from modoboa.lib.paginator import Paginator +from modoboa.lib.viewsets import HasMailbox from modoboa.webmail import lib, serializers from modoboa.webmail.lib import attachments class UserMailboxViewSet(viewsets.GenericViewSet): - permission_classes = (IsAuthenticated,) + + permission_classes = (IsAuthenticated, HasMailbox) def get_serializer_class(self): if self.action == "list": @@ -101,6 +103,8 @@ class UserMailboxViewSet(viewsets.GenericViewSet): class UserEmailViewSet(viewsets.GenericViewSet): + permission_classes = (IsAuthenticated, HasMailbox) + def get_serializer_class(self): if self.action == "list": return serializers.PaginatedEmailListSerializer @@ -262,6 +266,8 @@ class UserEmailViewSet(viewsets.GenericViewSet): class ComposeSessionViewSet(viewsets.GenericViewSet): + permission_classes = (IsAuthenticated, HasMailbox) + def get_serializer_class(self): if self.action == "send": return serializers.SendEmailSerializer