Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
actions: read
steps:
- uses: actions/checkout@v6
- uses: github/codeql-action/init@v3
- uses: github/codeql-action/init@v4
with:
languages: python
queries: security-extended,security-and-quality
- uses: github/codeql-action/analyze@v3
- uses: github/codeql-action/analyze@v4
with:
category: "/language:python"
2 changes: 2 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
with:
python-version: "3.13"
cache: 'pip'
- run: python -m pip install --upgrade pip
- run: pip install ruff bandit mypy pip-audit
# Fast checks
- run: ruff check .
Expand All @@ -43,6 +44,7 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with: { python-version: "3.13" }
- run: python -m pip install --upgrade pip
- run: pip install pip-audit
- run: pip-audit --strict

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ We [keep a changelog.](http://keepachangelog.com/)

## [Unreleased]

## [1.7.0] - 2026-06-10

### Security

- **Enterprise Runtime Security:** Added opt-in PEP 578 Audit Hooks (`sys.addaudithook`) via `Config.enable_security_audit` to track runtime network events.
Expand Down Expand Up @@ -38,6 +40,12 @@ We [keep a changelog.](http://keepachangelog.com/)

- Legacy compatibility: Restored parity with old exceptions and dynamic routing mechanics to keep integration completely seamless for existing users.

### Pull Requests Merged

- [PR_129](https://github.com/mailjet/mailjet-apiv3-python/pull/129) - Use hyphen in the package name in readme.
- [PR_130](https://github.com/mailjet/mailjet-apiv3-python/pull/130) - refactor: Modernize SDK architecture, harden security, and enable O(1) routing.
- [PR_131](https://github.com/mailjet/mailjet-apiv3-python/pull/131) - Release 1.7.0.

## [1.6.0] - 2026-04-27

### Security
Expand Down Expand Up @@ -295,4 +303,5 @@ We [keep a changelog.](http://keepachangelog.com/)
[1.5.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.5.0
[1.5.1]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.5.1
[1.6.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.6.0
[1.7.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.7.0
[unreleased]: https://github.com/mailjet/mailjet-apiv3-python/compare/v1.7.0...HEAD
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ include README.md
include CHANGELOG.md
include PERFORMANCE.md
include SECURITY.md
recursive-include mailjet_rest *.py *.pyi py.typed
recursive-include mailjet_rest *.py py.typed

prune tests
prune samples
Expand Down
2 changes: 1 addition & 1 deletion conda.recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ test:
- responses
commands:
- pip check
- pytest tests/unit/ -v -m "not property_heavy"
- pytest tests/unit/ -v

about:
home: {{ project['urls']['Homepage'] }}
Expand Down
2 changes: 1 addition & 1 deletion mailjet_rest/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.6.0.post1.dev23"
__version__ = "1.7.0"
15 changes: 15 additions & 0 deletions mailjet_rest/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@
from typing_extensions import NotRequired


__all__ = [
# Constants
"_ALLOWED_TRACE_FIELDS",
"_DEFAULT_TIMEOUT",
"_JSON_HEADERS",
"_TEXT_HEADERS",
"Attachment",
"EmailAddress",
"HttpMethod",
"PayloadType",
"SendV31Message",
"SendV31Payload",
"TimeoutType",
]

# ==========================================
# Types & Constants
# ==========================================
Expand Down
114 changes: 59 additions & 55 deletions manage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ test_integration() {

test_cov() {
# Example: ./manage.sh test_cov
info "Running tests with Coverage requirements (Fail under 80%)..."
info "Running tests with Coverage requirements (Fail under 80%)...."
pytest -n auto --cov="${SRC_DIR}" "${TEST_DIR}" --cov-fail-under=80 --cov-report=term-missing --cov-report=html
success "Coverage report generated in htmlcov/index.html"
}
Expand All @@ -108,26 +108,35 @@ test_strict_warnings() {
# SECURITY & FUZZING
# ==============================================================================
fuzz_all() {
# Usage: ./manage.sh fuzz_all [duration_in_seconds]
local duration=${1:-30}
# Usage: ./manage.sh fuzz_all [duration_in_seconds] [extra_libfuzzer_flags...]
local duration=${1:-20}
if [ $# -gt 0 ]; then shift; fi

local root_dir="$(pwd)"
local fuzzer_dir="tests/fuzz"
local dictionary="tests/fuzz/fuzzer.dict"
local corpus_dir="tests/fuzz/corpus"
local log_dir="logs"

if [ ! -d "$fuzzer_dir" ]; then
error "Fuzzer directory '$fuzzer_dir' not found."
return 1
fi

mkdir -p "$log_dir"

# Ensure the dictionary exists before passing the argument
local dict_arg=""
if [ -f "$dictionary" ]; then
dict_arg="-dict=$dictionary"
dict_arg="-dict=$root_dir/$dictionary"
else
echo "⚠️ Warning: Dictionary '$dictionary' not found. Running without it."
echo -e "${YELLOW}⚠️ Warning: Dictionary '$dictionary' not found. Running without it.${NC}"
fi

info "🚀 Starting security fuzzing suite (duration: ${duration} seconds per fuzzer)..."
if [ $# -gt 0 ]; then
info "Applying extra LibFuzzer arguments: $@"
fi

# Safely gather fuzzer files to prevent errors if none exist
shopt -s nullglob
Expand All @@ -141,27 +150,49 @@ fuzz_all() {

for fuzzer in "${fuzzers[@]}"; do
local fuzzer_name=$(basename "$fuzzer" .py)
local fuzzer_corpus="$corpus_dir/$fuzzer_name"

# Create dedicated directories for THIS specific fuzzer
local fuzzer_work_dir="$root_dir/$log_dir/$fuzzer_name"
local fuzzer_corpus="$root_dir/$corpus_dir/$fuzzer_name"

mkdir -p "$fuzzer_work_dir"
mkdir -p "$fuzzer_corpus"

info "🔍 Running fuzzer: $fuzzer (Corpus: $fuzzer_corpus)"

conda run --name "${CONDA_ENV_NAME}" python "$fuzzer" \
$dict_arg \
-max_len=512 \
-max_total_time="$duration" \
"$fuzzer_corpus"

local exit_code=$?
# libFuzzer returns 1 for crash, 70 for OOM, 77 for timeout.
# Catching any non-zero exit ensures we don't miss Python tracebacks.
if [ $exit_code -ne 0 ]; then
error "❌ Fuzzing failed: Crash or error detected in $fuzzer (Exit Code: $exit_code)."
return $exit_code
fi
echo -e "🔍 Running fuzzer: $fuzzer_name (Logs: logs/$fuzzer_name/fuzz_output.log)"

# Isolate execution:
# 1. Background subshell `(...) &` runs fuzzers in parallel.
# 2. `cd` into log folder ensures LibFuzzer outputs (crash, leak files) are neatly contained.
# 3. Use absolute paths for the execution dependencies to avoid pathing errors.
(
cd "$fuzzer_work_dir"

# Prevent Conda's double-stack path bug by checking if we are already activated
if [[ "$CONDA_DEFAULT_ENV" == "${CONDA_ENV_NAME}" ]]; then
EXEC_CMD="python"
else
EXEC_CMD="conda run --name ${CONDA_ENV_NAME} python"
fi

$EXEC_CMD "$root_dir/$fuzzer" \
$dict_arg \
-max_len=512 \
-max_total_time="$duration" \
-artifact_prefix="./" \
"$fuzzer_corpus" \
"$@" > "fuzz_output.log" 2>&1

local exit_code=$?
# libFuzzer returns 1 for crash, 70 for OOM, 77 for timeout.
if [ $exit_code -ne 0 ]; then
echo -e "${RED}❌ Fuzzing failed: Crash or error detected in $fuzzer_name (Exit Code: $exit_code). Check logs/$fuzzer_name/fuzz_output.log${NC}"
fi
) &
done

success "✅ All fuzz tests passed successfully."
echo -e "${CYAN}⏳ All fuzzers launched in isolation. Waiting for completion...${NC}"
wait
success "✅ All fuzz tests finished."
}

# ==============================================================================
Expand Down Expand Up @@ -243,6 +274,7 @@ clean() {
find . -type f -name '*.egg' -exec rm -f {} +

# Temp logs and profilers
rm -rf logs/
rm -f *.prof profile.html profile.json tmp.txt wget-log

success "Workspace cleaned!"
Expand Down Expand Up @@ -270,7 +302,7 @@ help() {
echo " test_strict_warnings - Run tests and fail on any DeprecationWarning"
echo ""
echo -e "${YELLOW}Security & Fuzzing:${NC}"
echo " fuzz_all - Run all fuzz tests"
echo " fuzz_all - Run all fuzz tests (pass duration as first arg, optionally pass fuzzer flags)"
echo ""
echo -e "${YELLOW}Performance & Security:${NC}"
echo " perf_bench - Run pytest-benchmark suite"
Expand All @@ -286,7 +318,7 @@ help() {
echo -e "${GREEN}Examples:${NC}"
echo " ./manage.sh test_unit -vvv -s"
echo " ./manage.sh test_unit -k \"test_pep578_audit_hooks\""
echo " ./manage.sh test_no_warnings tests/unit/test_client.py"
echo " ./manage.sh fuzz_all 60 -max_len=16384"
}

# Check if at least one argument is provided
Expand All @@ -299,39 +331,11 @@ COMMAND=$1
shift # Remove the command from the arguments list, leaving only extra flags

case "$COMMAND" in
env_setup|format|lint|test_all|test_unit|test_integration|test_cov|test_no_warnings|test_strict_warnings|perf_bench|perf_profile|audit_deps|run_hooks|build_pkg|release|clean|help)
env_setup|format|lint|test_all|test_unit|test_integration|test_cov|test_no_warnings|test_strict_warnings|perf_bench|perf_profile|audit_deps|run_hooks|build_pkg|release|clean|fuzz_all)
"$COMMAND" "$@" # Execute the function with any remaining arguments
;;
fuzz_all)
# 1. Grab the duration, defaulting to 20 if not provided
DURATION=${1:-20}

# 2. Shift the duration out of the arguments list (if it was provided)
# This leaves ONLY the extra flags (like -max_len=16384) in "$@"
if [ $# -gt 0 ]; then shift; fi

info "🚀 Starting security fuzzing suite (duration: ${DURATION} seconds per fuzzer)..."
if [ $# -gt 0 ]; then
info "Applying extra LibFuzzer arguments: $@"
fi

# Find all fuzzers
FUZZERS=$(find tests/fuzz -maxdepth 1 -name "fuzz_*.py" -type f)

for fuzzer in $FUZZERS; do
fuzzer_name=$(basename "$fuzzer" .py)
corpus_dir="tests/fuzz/corpus/$fuzzer_name"

mkdir -p "$corpus_dir"

info "🔍 Running fuzzer: $fuzzer (Corpus: $corpus_dir)"

# 3. Append "$@" to the end to forward any extra arguments
python "$fuzzer" "$corpus_dir" -dict=tests/fuzz/fuzzer.dict -max_total_time="$DURATION" "$@"

echo ""
done
success "✅ All fuzz tests passed successfully."
help)
help
;;
*)
error "Unknown command: $COMMAND"
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ py-modules = ["mailjet_rest._version"]

[tool.setuptools.packages.find]
include = ["mailjet_rest", "mailjet_rest.*"]
exclude = ["tests*"]

[tool.setuptools.package-data]
mailjet_rest = ["py.typed", "*.pyi"]
mailjet_rest = ["py.typed"]

[project]
name = "mailjet-rest"
Expand Down
1 change: 0 additions & 1 deletion samples/smoke_readme_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import os
import uuid
import logging
import warnings
import time

from mailjet_rest import Client, MailjetAuthError
Expand Down
2 changes: 1 addition & 1 deletion tests/fuzz/fuzz_state_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def TestOneInput(data: bytes) -> None:
builder._msg = {}
elif op == 3:
payload = builder.build()
client._execute_request("POST", "https://api.mailjet.com/v3/send", data=payload) # type: ignore[call-arg]
client.api_call("POST", "https://api.mailjet.com/v3/send", data=payload)
elif op == 4:
client.auth = (fdp.ConsumeUnicodeNoSurrogates(5), None) # type: ignore[attr-defined]

Expand Down
Loading
Loading