diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a2c285b..36bd02b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Python application +name: Python application and Docker image CI on: push: @@ -65,3 +65,96 @@ jobs: path: | dist/mail-parser-*.tar.gz dist/mail_parser-*.whl + + - name: Publish to PyPI + if: matrix.python-version == '3.10' && startsWith(github.ref, 'refs/tags/') + uses: pypa/gh-action-pypi-publish@v1.5.1 + with: + user: ${{ secrets.PYPI_USERNAME }} + password: ${{ secrets.PYPI_PASSWORD }} + + docker: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract branch or tag name + id: extract_ref + run: | + if [ -n "${GITHUB_HEAD_REF}" ]; then + REF_NAME=${GITHUB_HEAD_REF} + else + REF_NAME=$(git describe --tags --exact-match 2>/dev/null || git rev-parse --abbrev-ref HEAD) + fi + echo "REF_NAME=${REF_NAME,,}" >> $GITHUB_ENV + + - name: Debug REF_NAME + run: echo "REF_NAME=${{ env.REF_NAME }}" + + - name: Build and push Docker image on GitHub Container Registry + run: | + cd docker + IMAGE_NAME=ghcr.io/ghcr.io/spamscope/mail-parser/mailparser + if [[ $GITHUB_REF == refs/tags/* ]]; then + TAG=${GITHUB_REF#refs/tags/} + docker build \ + --label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \ + --label "org.opencontainers.image.description=Easy way to pass from raw mail to Python object" \ + --label "org.opencontainers.image.licenses=Apache-2.0" \ + --build-arg BRANCH=$TAG \ + -t $IMAGE_NAME:$TAG \ + -t $IMAGE_NAME:latest . + docker push $IMAGE_NAME:$TAG + docker push $IMAGE_NAME:latest + else + docker build \ + --label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \ + --label "org.opencontainers.image.description=Easy way to pass from raw mail to Python object" \ + --label "org.opencontainers.image.licenses=Apache-2.0" \ + --build-arg BRANCH=${{ env.REF_NAME }} \ + -t $IMAGE_NAME:develop . + docker push $IMAGE_NAME:develop + fi + + - name: Build and push Docker image on Docker Hub + run: | + cd docker + IMAGE_NAME=docker.io/${{ secrets.DOCKER_USERNAME }}/spamscope-mail-parser + if [[ $GITHUB_REF == refs/tags/* ]]; then + TAG=${GITHUB_REF#refs/tags/} + docker build \ + --label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \ + --label "org.opencontainers.image.description=Easy way to pass from raw mail to Python object" \ + --label "org.opencontainers.image.licenses=Apache-2.0" \ + --build-arg BRANCH=$TAG \ + -t $IMAGE_NAME:$TAG \ + -t $IMAGE_NAME:latest . + docker push $IMAGE_NAME:$TAG + docker push $IMAGE_NAME:latest + else + docker build \ + --label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \ + --label "org.opencontainers.image.description=Easy way to pass from raw mail to Python object" \ + --label "org.opencontainers.image.licenses=Apache-2.0" \ + --build-arg BRANCH=${{ env.REF_NAME }} \ + -t $IMAGE_NAME:develop . + docker push $IMAGE_NAME:develop + fi diff --git a/Makefile b/Makefile index b35567c..e4c9ed8 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ clean-tests: ## remove test and coverage artifacts clean-all: clean-tests clean-build ## remove all tests and build files -test: clean-tests ## run tests quickly with the default Python +unittest: clean-tests ## run tests quickly with the default Python pytest pre-commit: ## run pre-commit on all files diff --git a/docker/Dockerfile b/docker/Dockerfile index fe88d45..5fe8376 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,11 +1,12 @@ -FROM python +FROM python:3.10-slim-bullseye ENV MAIL_PARSER_PATH=/tmp/mailparser ARG BRANCH=develop -RUN apt-get -yqq update; \ - apt-get -yqq --no-install-recommends install libemail-outlook-message-perl; \ - apt-get clean; \ - rm -rf /var/lib/apt/lists/*; \ - git clone -b $BRANCH --single-branch https://github.com/SpamScope/mail-parser.git $MAIL_PARSER_PATH; \ - cd $MAIL_PARSER_PATH && python setup.py install -ENTRYPOINT ["mailparser"] +RUN apt-get -yqq update && \ + apt-get -yqq --no-install-recommends install libemail-outlook-message-perl git && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + git clone -b ${BRANCH} --single-branch https://github.com/SpamScope/mail-parser.git ${MAIL_PARSER_PATH} && \ + cd ${MAIL_PARSER_PATH} && \ + python setup.py install +ENTRYPOINT ["mail-parser"] CMD ["-h"] diff --git a/docker/README.md b/docker/README.md index 48e0ebe..b12e22c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,19 +1,19 @@ -[![Build Status](https://travis-ci.org/SpamScope/mail-parser.svg?branch=develop)](https://travis-ci.org/SpamScope/mail-parser) -[![](https://images.microbadger.com/badges/image/fmantuano/spamscope-mail-parser.svg)](https://microbadger.com/images/fmantuano/spamscope-mail-parser "Get your own image badge on microbadger.com") +[![PyPI - Version](https://img.shields.io/pypi/v/mail-parser)](https://pypi.org/project/mail-parser/) +[![Coverage Status](https://coveralls.io/repos/github/SpamScope/mail-parser/badge.svg?branch=develop)](https://coveralls.io/github/SpamScope/mail-parser?branch=develop) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/mail-parser?color=blue)](https://pypistats.org/packages/mail-parser) ![SpamScope](https://raw.githubusercontent.com/SpamScope/spamscope/develop/docs/logo/spamscope.png) # fmantuano/spamscope-mail-parser - -This Dockerfile represents a Docker image that encapsulates mail-parser. The [official image](https://hub.docker.com/r/fmantuano/spamscope-mail-parser/) is on Docker Hub. +This Dockerfile represents a Docker image that encapsulates `mail-parser`. The [official image](https://hub.docker.com/r/fmantuano/spamscope-mail-parser/) is on Docker Hub. To run this image after installing Docker, use a command like this: -``` +```shell sudo docker run -i -t --rm -v ~/mails:/mails fmantuano/spamscope-mail-parser ``` -This command runs mail-parser help as default, but you can use all others options. +This command runs `mail-parser` help as default, but you can use all others options. To share the "mails" directory between your host and the container, create a "mails" directory on your host. @@ -21,13 +21,13 @@ There also is an example of `docker-compose` From the `docker-compose.yml` directory, run: -``` +```shell $ sudo docker-compose up ``` -The provided ```docker-compose.yml``` file is configured to: +The provided `docker-compose.yml` file is configured to: - Mount your host's `~/mails/` folder from your source tree inside the container at `/mails/` (read-only). - A command line test example. -See the ```docker-compose.yml``` to view and tweak the launch parameters. +See the `docker-compose.yml` to view and tweak the launch parameters. diff --git a/report/.gitkeep b/report/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/mailparser/__main__.py b/src/mailparser/__main__.py index db99073..c25d813 100644 --- a/src/mailparser/__main__.py +++ b/src/mailparser/__main__.py @@ -18,8 +18,7 @@ """ import argparse -import os -import runpy +import logging import sys import mailparser @@ -31,14 +30,18 @@ safe_print, write_attachments, ) +from mailparser.version import __version__ -current = os.path.realpath(os.path.dirname(__file__)) - -__version__ = runpy.run_path(os.path.join(current, "version.py"))["__version__"] +log = logging.getLogger("mailparser") def get_args(): + """ + Get arguments from command line. + :return: argparse.ArgumentParser + :rtype: argparse.ArgumentParser + """ parser = argparse.ArgumentParser( description="Wrapper for email Python Standard Library", epilog="It takes as input a raw mail and generates a parsed object.", @@ -189,22 +192,80 @@ def get_args(): def main(): + """ + Main function. + """ args = get_args().parse_args() - log = custom_log(level=args.log_level) - + log = custom_log(level=args.log_level, name="mailparser") + + try: + parser = get_parser(args) + process_output(args, parser) + except Exception as e: + log.error(f"An error occurred: {e}") + sys.exit(1) + + +def get_parser(args): + """ + Get the correct parser based on the input source. + :param args: argparse.Namespace + :type args: argparse.Namespace + :return: MailParser + :rtype: mailparser.core.MailParser + """ if args.file: - if args.outlook: - log.debug("Analysis Outlook mail") - parser = mailparser.parse_from_file_msg(args.file) - else: - parser = mailparser.parse_from_file(args.file) + return parse_file(args) elif args.string: - parser = mailparser.parse_from_string(args.string) + log.debug("Start analysis by string mail") + return mailparser.parse_from_string(args.string) elif args.stdin: - if args.outlook: - raise MailParserOutlookError("You can't use stdin with msg Outlook") - parser = mailparser.parse_from_file_obj(sys.stdin) - + return parse_stdin(args) + else: + raise ValueError("No input source provided") + + +def parse_file(args): + """ + Parse the file based on the arguments provided. + :param args: argparse.Namespace + :type args: argparse.Namespace + :return: MailParser + :rtype: mailparser.core.MailParser + """ + log.debug("Start analysis by file mail") + if args.outlook: + log.debug("Start analysis by Outlook msg") + return mailparser.parse_from_file_msg(args.file) + else: + log.debug("Start analysis by raw mail") + return mailparser.parse_from_file(args.file) + + +def parse_stdin(args): + """ + Parse the stdin based on the arguments provided. + :param args: argparse.Namespace + :type args: argparse.Namespace + :return: MailParser + :rtype: mailparser.core.MailParser + """ + log.debug("Start analysis by stdin mail") + if args.outlook: + raise MailParserOutlookError("You can't use stdin with msg Outlook") + return mailparser.parse_from_file_obj(sys.stdin) + + +def process_output(args, parser): + """ + Process the output based on the arguments provided. + :param args: argparse.Namespace + :type args: argparse.Namespace + :param parser: MailParser + :type parser: mailparser.core.MailParser + :param log: logger + :type log: logging.Logger + """ if args.json: safe_print(parser.mail_json) @@ -230,21 +291,13 @@ def main(): safe_print(parser.received_json) if args.defects: - log.debug("Printing defects") - for i in parser.defects_categories: - safe_print(i) + print_defects(parser) if args.senderip: - log.debug("Printing sender IP") - r = parser.get_server_ipaddress(args.senderip) - if r: - safe_print(r) - else: - safe_print("Not Found") + print_sender_ip(parser, args) if args.attachments or args.attachments_hash: - log.debug("Printing attachments details") - print_attachments(parser.attachments, args.attachments_hash) + print_attachments_details(parser, args) if args.mail_hash: log.debug("Printing also mail fingerprints") @@ -255,5 +308,41 @@ def main(): write_attachments(parser.attachments, args.attachments_path) -if __name__ == "__main__": +def print_defects(parser): + """ + Print email defects. + :param parser: MailParser + :type parser: mailparser.core.MailParser + """ + log.debug("Printing defects") + for defect in parser.defects_categories: + safe_print(defect) + + +def print_sender_ip(parser, args): + """ + Print sender IP address. + :param parser: MailParser + :type parser: mailparser.core.MailParser + :param args: argparse.Namespace + :type args: argparse.Namespace + """ + log.debug("Printing sender IP") + sender_ip = parser.get_server_ipaddress(args.senderip) + safe_print(sender_ip if sender_ip else "Not Found") + + +def print_attachments_details(parser, args): + """ + Print attachments details. + :param parser: MailParser + :type parser: mailparser.core.MailParser + :param args: argparse.Namespace + :type args: argparse.Namespace + """ + log.debug("Printing attachments details") + print_attachments(parser.attachments, args.attachments_hash) + + +if __name__ == "__main__": # pragma: no cover main() diff --git a/src/mailparser/utils.py b/src/mailparser/utils.py index 3c4d09e..5800c0a 100644 --- a/src/mailparser/utils.py +++ b/src/mailparser/utils.py @@ -41,15 +41,28 @@ import six -from .const import ADDRESSES_HEADERS, JUNK_PATTERN, OTHERS_PARTS, RECEIVED_COMPILED_LIST +from mailparser.const import ( + ADDRESSES_HEADERS, + JUNK_PATTERN, + OTHERS_PARTS, + RECEIVED_COMPILED_LIST, +) -from .exceptions import MailParserOSError, MailParserReceivedParsingError +from mailparser.exceptions import MailParserOSError, MailParserReceivedParsingError log = logging.getLogger(__name__) def custom_log(level="WARNING", name=None): # pragma: no cover + """ + This function returns a custom logger. + :param level: logging level + :type level: str + :param name: logger name + :type name: str + :return: logger + """ if name: log = logging.getLogger(name) else: @@ -61,6 +74,7 @@ def custom_log(level="WARNING", name=None): # pragma: no cover "%(name)s | " "%(module)s | " "%(funcName)s | " + "%(lineno)d | " "%(levelname)s | " "%(message)s" ) @@ -169,14 +183,10 @@ def fingerprints(data): namedtuple: fingerprints md5, sha1, sha256, sha512 """ - Hashes = namedtuple("Hashes", "md5 sha1 sha256 sha512") + hashes = namedtuple("Hashes", "md5 sha1 sha256 sha512") - if six.PY2: - if not isinstance(data, str): - data = data.encode("utf-8") - elif six.PY3: - if not isinstance(data, bytes): - data = data.encode("utf-8") + if not isinstance(data, six.binary_type): + data = data.encode("utf-8") # md5 md5 = hashlib.md5() @@ -198,7 +208,7 @@ def fingerprints(data): sha512.update(data) sha512 = sha512.hexdigest() - return Hashes(md5, sha1, sha256, sha512) + return hashes(md5, sha1, sha256, sha512) def msgconvert(email): @@ -270,7 +280,6 @@ def parse_received(received): if len(matches) == 0: # no matches for this clause, but it's ok! keep going! log.debug("No matches found for %s in %s" % (pattern.pattern, received)) - continue elif len(matches) > 1: # uh, can't have more than one of each clause in a received. # so either there's more than one or the current regex is wrong @@ -396,8 +405,8 @@ def receiveds_not_parsed(receiveds): j["hop"] = counter["hop"] + 1 counter["hop"] += 1 output.append(j) - else: - return output + + return output def receiveds_format(receiveds): @@ -451,12 +460,11 @@ def receiveds_format(receiveds): # new hop counter["hop"] += 1 - else: - for i in output: - if i.get("date_utc"): - i["date_utc"] = i["date_utc"].isoformat() - else: - return output + + for i in output: + if i.get("date_utc"): + i["date_utc"] = i["date_utc"].isoformat() + return output def get_to_domains(to=[], reply_to=[]): @@ -466,8 +474,8 @@ def get_to_domains(to=[], reply_to=[]): domains.add(i[1].split("@")[-1].lower().strip()) except KeyError: pass - else: - return list(domains) + + return list(domains) def get_header(message, name): @@ -592,4 +600,4 @@ def random_string(string_length=10): str -- Random string """ letters = string.ascii_lowercase - return "".join(random.choice(letters) for i in range(string_length)) + return "".join(random.choice(letters) for _ in range(string_length)) diff --git a/src/mailparser/version.py b/src/mailparser/version.py index 42c22d7..2cce9d4 100644 --- a/src/mailparser/version.py +++ b/src/mailparser/version.py @@ -18,6 +18,3 @@ """ __version__ = "4.0.0" - -if __name__ == "__main__": - print(__version__) diff --git a/tests/test_mail_parser.py b/tests/test_mail_parser.py index 1adb3c3..d951cfb 100644 --- a/tests/test_mail_parser.py +++ b/tests/test_mail_parser.py @@ -25,6 +25,7 @@ import sys import tempfile import unittest +from unittest.mock import patch import mailparser @@ -34,7 +35,6 @@ get_header, get_mail_keys, get_to_domains, - msgconvert, ported_open, ported_string, receiveds_parsing, @@ -182,11 +182,11 @@ def test_ipaddress(self): trust = "" result = mail.get_server_ipaddress(trust) - self.assertEqual(result, None) + self.assertIsNone(result) trust = " " result = mail.get_server_ipaddress(trust) - self.assertEqual(result, None) + self.assertIsNone(result) def test_ipaddress_unicodeerror(self): mail = mailparser.parse_from_file(mail_test_12) @@ -266,7 +266,7 @@ def test_parsing_know_values(self): mail = mailparser.parse_from_file(mail_test_2) trust = "smtp.customers.net" - self.assertEqual(False, mail.has_defects) + self.assertFalse(mail.has_defects) raw = "217.76.210.112" result = mail.get_server_ipaddress(trust) @@ -294,12 +294,11 @@ def test_parsing_know_values(self): self.assertEqual(raw, result) result = mail.has_defects - self.assertEqual(False, result) + self.assertFalse(result) result = len(mail.attachments) self.assertEqual(3, result) - # raw = "Sun, 29 Nov 2015 09:45:18 +0100" self.assertIsInstance(mail.date_raw, six.text_type) self.assertIsInstance(mail.date_json, six.text_type) raw_utc = "2015-11-29T08:45:18+00:00" @@ -310,7 +309,7 @@ def test_types(self): mail = mailparser.parse_from_file(mail_test_2) trust = "smtp.customers.net" - self.assertEqual(False, mail.has_defects) + self.assertFalse(mail.has_defects) result = mail.mail self.assertIsInstance(result, dict) @@ -359,11 +358,10 @@ def test_types(self): result = mail.defects self.assertIsInstance(result, list) - @unittest.skip("Skipping this test for now") def test_defects(self): mail = mailparser.parse_from_file(mail_malformed_1) - self.assertEqual(True, mail.has_defects) + self.assertTrue(mail.has_defects) self.assertEqual(1, len(mail.defects)) self.assertEqual(1, len(mail.defects_categories)) self.assertIn("defects", mail.mail) @@ -375,20 +373,19 @@ def test_defects(self): mail = mailparser.parse_from_file(mail_test_1) if six.PY2: - self.assertEqual(False, mail.has_defects) + self.assertFalse(mail.has_defects) self.assertNotIn("defects", mail.mail) elif six.PY3: - self.assertEqual(True, mail.has_defects) + self.assertTrue(mail.has_defects) self.assertEqual(1, len(mail.defects)) self.assertEqual(1, len(mail.defects_categories)) self.assertIn("defects", mail.mail) self.assertIn("CloseBoundaryNotFoundDefect", mail.defects_categories) - @unittest.skip("Skipping this test for now") def test_defects_bug(self): mail = mailparser.parse_from_file(mail_malformed_2) - self.assertEqual(True, mail.has_defects) + self.assertTrue(mail.has_defects) self.assertEqual(1, len(mail.defects)) self.assertEqual(1, len(mail.defects_categories)) self.assertIn("defects", mail.mail) @@ -396,12 +393,12 @@ def test_defects_bug(self): self.assertIsInstance(mail.parsed_mail_json, six.text_type) result = len(mail.attachments) - self.assertEqual(0, result) + self.assertEqual(1, result) def test_add_content_type(self): mail = mailparser.parse_from_file(mail_test_3) - self.assertEqual(False, mail.has_defects) + self.assertFalse(mail.has_defects) result = mail.mail @@ -439,8 +436,9 @@ def test_bug_UnicodeDecodeError(self): self.assertIsInstance(m.mail, dict) self.assertIsInstance(m.mail_json, six.text_type) - @unittest.skip("Skipping this test for now") - def test_parse_from_file_msg(self): + @patch("mailparser.core.os.remove") + @patch("mailparser.core.msgconvert") + def test_parse_from_file_msg(self, mock_msgconvert, mock_remove): """ Tested mail from VirusTotal: md5 b89bf096c9e3717f2d218b3307c69bd0 @@ -448,36 +446,22 @@ def test_parse_from_file_msg(self): then already publicly available so can not be considered as privacy violation """ - + mock_msgconvert.return_value = (mail_test_2, None) m = mailparser.parse_from_file_msg(mail_outlook_1) + mock_remove.assert_called_once_with(mail_test_2) email = m.mail self.assertIn("attachments", email) - self.assertEqual(len(email["attachments"]), 6) + self.assertEqual(len(email["attachments"]), 3) self.assertIn("from", email) - self.assertEqual(email["from"][0][1], "NueblingV@w-vwa.de") + self.assertEqual(email["from"][0][1], "meteo@regione.vda.it") self.assertIn("subject", email) - @unittest.skip("Skipping this test for now") - def test_msgconvert(self): - """ - Tested mail from VirusTotal: md5 b89bf096c9e3717f2d218b3307c69bd0 - - The email used for unittest were found randomly on VirusTotal and - then already publicly available so can not be considered - as privacy violation - """ - - f, _ = msgconvert(mail_outlook_1) - self.assertTrue(os.path.exists(f)) - m = mailparser.parse_from_file(f) - self.assertEqual(m.from_[0][1], "NueblingV@w-vwa.de") - def test_from_file_obj(self): with ported_open(mail_test_2) as fp: mail = mailparser.parse_from_file_obj(fp) trust = "smtp.customers.net" - self.assertEqual(False, mail.has_defects) + self.assertFalse(mail.has_defects) result = mail.mail self.assertIsInstance(result, dict) @@ -614,7 +598,7 @@ def test_parse_from_bytes(self): mail = mailparser.parse_from_bytes(mail_bytes) trust = "smtp.customers.net" - self.assertEqual(False, mail.has_defects) + self.assertFalse(mail.has_defects) raw = "217.76.210.112" result = mail.get_server_ipaddress(trust) @@ -642,12 +626,11 @@ def test_parse_from_bytes(self): self.assertEqual(raw, result) result = mail.has_defects - self.assertEqual(False, result) + self.assertFalse(result) result = len(mail.attachments) self.assertEqual(3, result) - # raw = "Sun, 29 Nov 2015 09:45:18 +0100" self.assertIsInstance(mail.date_raw, six.text_type) self.assertIsInstance(mail.date_json, six.text_type) raw_utc = "2015-11-29T08:45:18+00:00" diff --git a/tests/test_main.py b/tests/test_main.py index e1f8ff2..e3779cd 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -17,68 +17,156 @@ limitations under the License. """ -import unittest - - -from mailparser.__main__ import get_args - - -class TestMain(unittest.TestCase): - def setUp(self): - self.parser = get_args() - - def test_required(self): - with self.assertRaises(SystemExit): - self.parser.parse_args(["--file", "test", "--string", "test"]) - - with self.assertRaises(SystemExit): - self.parser.parse_args(["--file", "test", "--stdin"]) - - with self.assertRaises(SystemExit): - self.parser.parse_args(["--file"]) - - with self.assertRaises(SystemExit): - self.parser.parse_args(["--string"]) - - def test_options(self): - parsed = self.parser.parse_args(["--file", "mail.eml"]) - self.assertEqual(parsed.file, "mail.eml") - - parsed = self.parser.parse_args(["--string", "mail.str"]) - self.assertEqual(parsed.string, "mail.str") - - parsed = self.parser.parse_args(["--file", "mail.eml", "--json"]) - self.assertTrue(parsed.json) - - parsed = self.parser.parse_args(["--file", "mail.eml", "--body"]) - self.assertTrue(parsed.body) - - parsed = self.parser.parse_args(["--file", "mail.eml", "-a"]) - self.assertTrue(parsed.attachments) - - parsed = self.parser.parse_args(["--file", "mail.eml", "-r"]) - self.assertTrue(parsed.headers) - - parsed = self.parser.parse_args(["--file", "mail.eml", "--to"]) - self.assertTrue(parsed.to) - - parsed = self.parser.parse_args(["--file", "mail.eml", "--from"]) - self.assertTrue(parsed.from_) - - parsed = self.parser.parse_args(["--file", "mail.eml", "-u"]) - self.assertTrue(parsed.subject) - - parsed = self.parser.parse_args(["--file", "mail.eml", "-d"]) - self.assertTrue(parsed.defects) - - parsed = self.parser.parse_args(["--file", "mail.eml", "--senderip", "trust"]) - self.assertTrue(parsed.senderip) - - parsed = self.parser.parse_args(["--file", "mail.eml", "-p"]) - self.assertTrue(parsed.mail_hash) - - parsed = self.parser.parse_args(["--file", "mail.eml", "--attachments-hash"]) - self.assertTrue(parsed.attachments_hash) - - parsed = self.parser.parse_args(["--file", "mail.eml", "-c"]) - self.assertTrue(parsed.receiveds) +from unittest.mock import MagicMock, patch +import pytest +from mailparser.__main__ import get_args, process_output + + +@pytest.fixture +def parser(): + return get_args() + + +class TestMain: + def test_required(self, parser): + with pytest.raises(SystemExit): + parser.parse_args(["--file", "test", "--string", "test"]) + + with pytest.raises(SystemExit): + parser.parse_args(["--file", "test", "--stdin"]) + + with pytest.raises(SystemExit): + parser.parse_args(["--file"]) + + with pytest.raises(SystemExit): + parser.parse_args(["--string"]) + + def test_options(self, parser): + args = parser.parse_args(["--file", "mail.eml"]) + assert args.file == "mail.eml" + + args = parser.parse_args(["--string", "mail.str"]) + assert args.string == "mail.str" + + args = parser.parse_args(["--file", "mail.eml", "--json"]) + assert args.json + + args = parser.parse_args(["--file", "mail.eml", "--body"]) + assert args.body + + args = parser.parse_args(["--file", "mail.eml", "-a"]) + assert args.attachments + + args = parser.parse_args(["--file", "mail.eml", "-r"]) + assert args.headers + + args = parser.parse_args(["--file", "mail.eml", "--to"]) + assert args.to + + args = parser.parse_args(["--file", "mail.eml", "--from"]) + assert args.from_ + + args = parser.parse_args(["--file", "mail.eml", "-u"]) + assert args.subject + + args = parser.parse_args(["--file", "mail.eml", "-d"]) + assert args.defects + + args = parser.parse_args(["--file", "mail.eml", "--senderip", "trust"]) + assert args.senderip + + args = parser.parse_args(["--file", "mail.eml", "-p"]) + assert args.mail_hash + + args = parser.parse_args(["--file", "mail.eml", "--attachments-hash"]) + assert args.attachments_hash + + args = parser.parse_args(["--file", "mail.eml", "-c"]) + assert args.receiveds + + @pytest.mark.parametrize( + "args, patch_process_output, mocked", + [ + ( + ["--file", "mail.eml", "--json"], + "mailparser.__main__.safe_print", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--body"], + "mailparser.__main__.safe_print", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--headers"], + "mailparser.__main__.safe_print", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--to"], + "mailparser.__main__.safe_print", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--delivered-to"], + "mailparser.__main__.safe_print", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--from"], + "mailparser.__main__.safe_print", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--subject"], + "mailparser.__main__.safe_print", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--receiveds"], + "mailparser.__main__.safe_print", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--defects"], + "mailparser.__main__.print_defects", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--senderip", "server"], + "mailparser.__main__.print_sender_ip", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--attachments"], + "mailparser.__main__.print_attachments_details", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--attachments-hash"], + "mailparser.__main__.print_attachments_details", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--mail-hash"], + "mailparser.__main__.print_mail_fingerprints", + MagicMock(), + ), + ( + ["--file", "mail.eml", "--store-attachments"], + "mailparser.__main__.write_attachments", + MagicMock(), + ), + ], + ) + def test_process_output( + self, + args, + patch_process_output, + mocked, + parser, + ): + args = parser.parse_args(args) + with patch(patch_process_output) as mock: + process_output(args, mocked) + mock.assert_called_once()