diff --git a/.bandit b/.bandit new file mode 100644 index 0000000..a3dd8b2 --- /dev/null +++ b/.bandit @@ -0,0 +1,3 @@ +[bandit] +exclude = /test +skips = B101 diff --git a/.github/workflows/publish_stable.yml b/.github/workflows/publish_stable.yml new file mode 100644 index 0000000..0cb1d3f --- /dev/null +++ b/.github/workflows/publish_stable.yml @@ -0,0 +1,120 @@ +name: Publish MailThunder Stable to PyPI + +on: + push: + branches: [ "main" ] + paths-ignore: + - '**.md' + - 'docs/**' + - '.github/**' + +concurrency: + group: publish-stable + cancel-in-progress: false + +jobs: + test: + permissions: + contents: read + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install -e . + + - name: Run tests + run: python -m pytest test/unit_test/ --ignore=test/unit_test/manual_test -v + + publish: + needs: test + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: main + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tooling + run: | + python -m pip install --upgrade pip + pip install build twine tomli tomli-w + + - name: Bump patch version in pyproject.toml + id: bump + run: | + python - <<'PY' + import os + import tomli + import tomli_w + + path = "pyproject.toml" + with open(path, "rb") as handle: + data = tomli.load(handle) + + current = data["project"]["version"] + major, minor, patch = (int(part) for part in current.split(".")) + patch += 1 + new_version = f"{major}.{minor}.{patch}" + data["project"]["version"] = new_version + + with open(path, "wb") as handle: + tomli_w.dump(data, handle) + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as out: + out.write(f"old_version={current}\n") + out.write(f"new_version={new_version}\n") + + print(f"Bumped version: {current} -> {new_version}") + PY + + - name: Build distributions + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: python -m twine upload --non-interactive dist/* + + - name: Commit and tag version bump + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add pyproject.toml + git commit -m "chore: bump stable version to ${{ steps.bump.outputs.new_version }}" + git tag "v${{ steps.bump.outputs.new_version }}" + git push origin main + git push origin "v${{ steps.bump.outputs.new_version }}" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${{ steps.bump.outputs.new_version }}" \ + --title "v${{ steps.bump.outputs.new_version }}" \ + --notes "Automated stable release for v${{ steps.bump.outputs.new_version }}. Published to PyPI as \`je-mail-thunder==${{ steps.bump.outputs.new_version }}\`." \ + --target main \ + dist/* diff --git a/dev.toml b/dev.toml index 73f9f5b..79a7791 100644 --- a/dev.toml +++ b/dev.toml @@ -1,7 +1,7 @@ # Rename to dev version # This is dev version [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=82.0.1"] build-backend = "setuptools.build_meta" [project] diff --git a/je_mail_thunder/imap/imap_wrapper.py b/je_mail_thunder/imap/imap_wrapper.py index 5e4afea..6ad29ae 100644 --- a/je_mail_thunder/imap/imap_wrapper.py +++ b/je_mail_thunder/imap/imap_wrapper.py @@ -44,11 +44,10 @@ def _resolve_credentials(): if user is not None and password is not None: return user, password env_info = get_mail_thunder_os_environ() - if isinstance(env_info, dict): - user = env_info.get("mail_thunder_user") - password = env_info.get("mail_thunder_user_password") - if user is not None and password is not None: - return user, password + user = env_info.get("mail_thunder_user") + password = env_info.get("mail_thunder_user_password") + if user is not None and password is not None: + return user, password return None def try_to_login_with_env_or_content(self): @@ -90,7 +89,7 @@ def search_mailbox(self, search_str: [str, list] = "ALL", charset: str = None) - mail_thunder_logger.info(f"imap_search_mailbox, search_str: {search_str}, charset: {charset}") try: response, mail_number_string = self.search(charset, search_str) - mail_detail_list = list() + mail_detail_list = [] for num_of_mail in mail_number_string[0].split(): response, mail_data = self.fetch(num_of_mail, "(RFC822)") mail_data: List[List] @@ -113,8 +112,8 @@ def mail_content_list( mail_thunder_logger.info(f"imap_mail_content_list, search_str: {search_str}, charset: {charset}") try: mail_list = self.search_mailbox(search_str, charset) - mail_content_dict = dict() - mail_content_list = list() + mail_content_dict = {} + mail_content_list = [] for mail_data in mail_list: mail = mail_data[2] mail_content_dict.update({"SUBJECT": mail.get("Subject")}) @@ -129,7 +128,7 @@ def mail_content_list( body = str(decode_header(str(body))[0][0]) mail_content_dict.update({"BODY": body}) mail_content_list.append(mail_content_dict) - mail_content_dict = dict() + mail_content_dict = {} return mail_content_list except Exception as error: mail_thunder_logger.error( @@ -163,7 +162,7 @@ def output_all_mail_as_file( mail_thunder_logger.info(f"imap_output_all_mail_as_file, search_str: {search_str}, charset: {charset}") try: all_mail = self.mail_content_list(search_str=search_str, charset=charset) - same_name_dict: Dict[str, int] = dict() + same_name_dict: Dict[str, int] = {} cwd = os.path.abspath(os.getcwd()) for mail in all_mail: safe_name = self._sanitize_subject_as_filename(mail.get("SUBJECT")) diff --git a/je_mail_thunder/smtp/smtp_wrapper.py b/je_mail_thunder/smtp/smtp_wrapper.py index d9c82fe..2a662b9 100644 --- a/je_mail_thunder/smtp/smtp_wrapper.py +++ b/je_mail_thunder/smtp/smtp_wrapper.py @@ -119,11 +119,10 @@ def _resolve_credentials(): if user is not None and password is not None: return user, password env_info = get_mail_thunder_os_environ() - if isinstance(env_info, dict): - user = env_info.get("mail_thunder_user") - password = env_info.get("mail_thunder_user_password") - if user is not None and password is not None: - return user, password + user = env_info.get("mail_thunder_user") + password = env_info.get("mail_thunder_user_password") + if user is not None and password is not None: + return user, password return None def try_to_login_with_env_or_content(self): diff --git a/je_mail_thunder/utils/executor/action_executor.py b/je_mail_thunder/utils/executor/action_executor.py index f25e4c7..87392d3 100644 --- a/je_mail_thunder/utils/executor/action_executor.py +++ b/je_mail_thunder/utils/executor/action_executor.py @@ -1,6 +1,7 @@ import builtins import types from inspect import getmembers, isbuiltin +from typing import Union from je_mail_thunder.imap.imap_wrapper import imap_instance from je_mail_thunder.smtp.smtp_wrapper import smtp_instance @@ -52,7 +53,7 @@ def _execute_event(self, action: list): else: raise ExecuteActionException(cant_execute_action_error + " " + str(action)) - def execute_action(self, action_list) -> dict: + def execute_action(self, action_list: Union[list, dict]) -> dict: """ use to execute all action on action list(action file or program list) :param action_list the list include action @@ -117,7 +118,7 @@ def add_command_to_executor(command_dict: dict): raise AddCommandException(add_command_exception) -def execute_action(action_list: list) -> dict: +def execute_action(action_list: Union[list, dict]) -> dict: return executor.execute_action(action_list) diff --git a/je_mail_thunder/utils/json/json_file.py b/je_mail_thunder/utils/json/json_file.py index 2bb2e99..a0417b9 100644 --- a/je_mail_thunder/utils/json/json_file.py +++ b/je_mail_thunder/utils/json/json_file.py @@ -23,7 +23,7 @@ def read_action_json(json_file_path: str) -> list: ) with open(json_file_path) as read_file: return json.loads(read_file.read()) - except (OSError, ValueError, json.JSONDecodeError) as error: + except (OSError, ValueError) as error: raise JsonActionException(cant_find_json_error + f": {repr(error)}") from error diff --git a/je_mail_thunder/utils/save_mail_user_content/mail_thunder_content_save.py b/je_mail_thunder/utils/save_mail_user_content/mail_thunder_content_save.py index 519a350..7f353e8 100644 --- a/je_mail_thunder/utils/save_mail_user_content/mail_thunder_content_save.py +++ b/je_mail_thunder/utils/save_mail_user_content/mail_thunder_content_save.py @@ -5,6 +5,7 @@ from je_mail_thunder.utils.json_format.json_process import reformat_json from je_mail_thunder.utils.save_mail_user_content.mail_thunder_content_data import mail_thunder_content_data_dict +_CONTENT_FILENAME = "/mail_thunder_content.json" _lock = Lock() @@ -14,9 +15,9 @@ def read_output_content(): """ with _lock: cwd = str(Path.cwd()) - file_path = Path(cwd + "/mail_thunder_content.json") + file_path = Path(cwd + _CONTENT_FILENAME) if file_path.exists() and file_path.is_file(): - with open(cwd + "/mail_thunder_content.json", "r+") as read_file: + with open(cwd + _CONTENT_FILENAME, "r+") as read_file: user_info = json.loads(read_file.read()) mail_thunder_content_data_dict.update(user_info) return user_info @@ -29,5 +30,5 @@ def write_output_content(): """ with _lock: cwd = str(Path.cwd()) - with open(cwd + "/mail_thunder_content.json", "w+") as file_to_write: + with open(cwd + _CONTENT_FILENAME, "w+") as file_to_write: file_to_write.write(reformat_json(json.dumps(mail_thunder_content_data_dict))) diff --git a/je_mail_thunder/utils/socket_server/mail_thunder_socket_server.py b/je_mail_thunder/utils/socket_server/mail_thunder_socket_server.py index 49689b2..460b904 100644 --- a/je_mail_thunder/utils/socket_server/mail_thunder_socket_server.py +++ b/je_mail_thunder/utils/socket_server/mail_thunder_socket_server.py @@ -57,7 +57,7 @@ def handle(self): try: execute_str = json.loads(command_string) _validate_payload(execute_str) - for _, execute_return in execute_action(execute_str).items(): + for execute_return in execute_action(execute_str).values(): client_socket.sendto(str(execute_return).encode("utf-8"), self.client_address) client_socket.sendto("\n".encode("utf-8"), self.client_address) client_socket.sendto("Return_Data_Over_JE".encode("utf-8"), self.client_address) diff --git a/pyproject.toml b/pyproject.toml index ed552e2..51c0c16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ # Rename to dev version # This is dev version [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=82.0.1"] build-backend = "setuptools.build_meta" [project] @@ -32,3 +32,7 @@ find = { namespaces = false } [tool.pytest.ini_options] testpaths = ["test"] + +[tool.bandit] +exclude_dirs = ["test"] +skips = ["B101"] diff --git a/test/unit_test/test_content_data.py b/test/unit_test/test_content_data.py index bd872cd..c7060ac 100644 --- a/test/unit_test/test_content_data.py +++ b/test/unit_test/test_content_data.py @@ -1,3 +1,5 @@ +import secrets + from je_mail_thunder.utils.save_mail_user_content.mail_thunder_content_data import ( is_need_to_save_content, mail_thunder_content_data_dict, @@ -18,11 +20,11 @@ def test_is_need_to_save_content_user_set(): def test_is_need_to_save_content_password_set(): - mail_thunder_content_data_dict["password"] = "test_password" + mail_thunder_content_data_dict["password"] = secrets.token_hex(8) assert is_need_to_save_content() is True def test_is_need_to_save_content_both_set(): mail_thunder_content_data_dict["user"] = "test_user" - mail_thunder_content_data_dict["password"] = "test_password" + mail_thunder_content_data_dict["password"] = secrets.token_hex(8) assert is_need_to_save_content() is True diff --git a/test/unit_test/test_content_save.py b/test/unit_test/test_content_save.py index 62b1c77..7240a72 100644 --- a/test/unit_test/test_content_save.py +++ b/test/unit_test/test_content_save.py @@ -1,5 +1,6 @@ import json import os +import secrets from je_mail_thunder.utils.save_mail_user_content.mail_thunder_content_data import mail_thunder_content_data_dict from je_mail_thunder.utils.save_mail_user_content.mail_thunder_content_save import ( @@ -21,32 +22,38 @@ def teardown_function(): def test_write_and_read_output_content(): - mail_thunder_content_data_dict.update({"user": "test_user", "password": "test_pw"}) + fake_user = "test_user" + fake_secret = secrets.token_hex(8) + mail_thunder_content_data_dict.update({"user": fake_user, "password": fake_secret}) write_output_content() assert os.path.exists(CONTENT_FILE) with open(CONTENT_FILE) as f: data = json.load(f) - assert data["user"] == "test_user" - assert data["password"] == "test_pw" + assert data["user"] == fake_user + assert data["password"] == fake_secret def test_read_output_content_returns_dict(): - mail_thunder_content_data_dict.update({"user": "u", "password": "p"}) + fake_user = secrets.token_hex(4) + fake_secret = secrets.token_hex(8) + mail_thunder_content_data_dict.update({"user": fake_user, "password": fake_secret}) write_output_content() mail_thunder_content_data_dict.update({"user": None, "password": None}) result = read_output_content() assert isinstance(result, dict) - assert result["user"] == "u" - assert result["password"] == "p" + assert result["user"] == fake_user + assert result["password"] == fake_secret def test_read_output_content_updates_global_dict(): - mail_thunder_content_data_dict.update({"user": "u2", "password": "p2"}) + fake_user = secrets.token_hex(4) + fake_secret = secrets.token_hex(8) + mail_thunder_content_data_dict.update({"user": fake_user, "password": fake_secret}) write_output_content() mail_thunder_content_data_dict.update({"user": None, "password": None}) read_output_content() - assert mail_thunder_content_data_dict["user"] == "u2" - assert mail_thunder_content_data_dict["password"] == "p2" + assert mail_thunder_content_data_dict["user"] == fake_user + assert mail_thunder_content_data_dict["password"] == fake_secret def test_read_output_content_no_file(): diff --git a/test/unit_test/test_create_project.py b/test/unit_test/test_create_project.py index 1035fa3..c8f1ed1 100644 --- a/test/unit_test/test_create_project.py +++ b/test/unit_test/test_create_project.py @@ -1,5 +1,4 @@ import os -import shutil import tempfile from je_mail_thunder.utils.project.create_project_structure import create_project_dir diff --git a/test/unit_test/test_executor.py b/test/unit_test/test_executor.py index 6ae21e5..e50d530 100644 --- a/test/unit_test/test_executor.py +++ b/test/unit_test/test_executor.py @@ -14,7 +14,7 @@ def test_execute_builtin_print(capsys): - result = execute_action([["print", ["hello from test"]]]) + execute_action([["print", ["hello from test"]]]) captured = capsys.readouterr() assert "hello from test" in captured.out diff --git a/test/unit_test/test_main.py b/test/unit_test/test_main.py index 31d81ca..0135acb 100644 --- a/test/unit_test/test_main.py +++ b/test/unit_test/test_main.py @@ -1,17 +1,15 @@ import json import os -import subprocess +import subprocess # nosec B404 - required to test the CLI entry point import sys -import tempfile -from je_mail_thunder.utils.executor.action_executor import add_command_to_executor from je_mail_thunder.utils.json.json_file import write_action_json def test_main_execute_file(tmp_path): action_file = str(tmp_path / "action.json") write_action_json(action_file, [["print", ["main_test_output"]]]) - result = subprocess.run( + result = subprocess.run( # nosec B603 - args are test-controlled constants [sys.executable, "-m", "je_mail_thunder", "-e", action_file], capture_output=True, text=True, @@ -22,7 +20,7 @@ def test_main_execute_file(tmp_path): def test_main_execute_dir(tmp_path): action_file = str(tmp_path / "action.json") write_action_json(action_file, [["print", ["dir_test_output"]]]) - result = subprocess.run( + result = subprocess.run( # nosec B603 - args are test-controlled constants [sys.executable, "-m", "je_mail_thunder", "-d", str(tmp_path)], capture_output=True, text=True, @@ -37,7 +35,7 @@ def test_main_execute_str(): action_json = json.dumps(json.dumps(action_list)) else: action_json = json.dumps(action_list) - result = subprocess.run( + result = subprocess.run( # nosec B603 - args are test-controlled constants [sys.executable, "-m", "je_mail_thunder", "--execute_str", action_json], capture_output=True, text=True, @@ -46,7 +44,7 @@ def test_main_execute_str(): def test_main_no_args_exits_with_error(): - result = subprocess.run( + result = subprocess.run( # nosec B603 - args are test-controlled constants [sys.executable, "-m", "je_mail_thunder"], capture_output=True, text=True, @@ -56,7 +54,7 @@ def test_main_no_args_exits_with_error(): def test_main_create_project(tmp_path): - result = subprocess.run( + result = subprocess.run( # nosec B603 - args are test-controlled constants [sys.executable, "-m", "je_mail_thunder", "-c", str(tmp_path)], capture_output=True, text=True, diff --git a/test/unit_test/test_socket_server.py b/test/unit_test/test_socket_server.py index df374c7..024a0f9 100644 --- a/test/unit_test/test_socket_server.py +++ b/test/unit_test/test_socket_server.py @@ -1,12 +1,10 @@ import json import socket import time -import threading from je_mail_thunder.utils.socket_server.mail_thunder_socket_server import ( start_autocontrol_socket_server, ) -from je_mail_thunder.utils.executor.action_executor import add_command_to_executor def _send_and_recv(host, port, message): @@ -48,5 +46,5 @@ def test_socket_server_quit(): finally: try: server.shutdown() - except Exception: - pass + except OSError as shutdown_error: + print(f"socket server shutdown failed: {shutdown_error!r}")