Skip to content

feat(email-smtp): create Email (SMTP) send injector (#23) - #289

Draft
mariot wants to merge 21 commits into
mainfrom
feature/20-create-smtp
Draft

feat(email-smtp): create Email (SMTP) send injector (#23)#289
mariot wants to merge 21 commits into
mainfrom
feature/20-create-smtp

Conversation

@mariot

@mariot mariot commented Jul 6, 2026

Copy link
Copy Markdown
Member

Proposed changes

  • Add the Email (SMTP) injector with SMTP configuration, envelope sender, Reply-To, Cc/Bcc, and OpenAEV document attachments.
  • Use a 30 second SMTP timeout and a context manager, reject incomplete SMTP credentials before connecting, and reject header injection through standard-library email serialization.
  • Propagate the optional OpenAEV tenant id through the configuration samples and runtime config.
  • Validate attachment document identifiers before download and cover client, payload, and injector entrypoint flows with unit tests.

Testing Instructions

  1. Start a local SMTP sink: docker run --rm -p 1025:1025 -p 8025:8025 axllent/mailpit.
  2. Create config.yml from config.yml.sample or .env from .env.sample, then run poetry install and poetry run python -m email_smtp_injector.openaev_email_smtp.
  3. Create an atomic testing with the Email (SMTP) - Craft email contract and send through Mailpit on localhost:1025.
  4. Run the local quality checks: python -m unittest discover -s test, python -m pytest test, black --check ., isort --profile black --check ., and flake8 --ignore=E,W ..

Related issues

Checklist

  • I consider the submitted work as finished
  • I tested the code for its functionality
  • I wrote test cases for the relevant use case
  • I added or updated the relevant documentation
  • Where necessary I refactored code to improve the overall quality
  • For bug fix -> I implemented a test that covers the bug

Further comments

The do not merge label is intentional and remains in place as a maintainer-controlled merge-order gate for the email injector family. This PR has been approved only and was not merged.

@mariot
mariot requested review from Kakudou, Copilot and guzmud July 6, 2026 12:57
@mariot mariot self-assigned this Jul 6, 2026
@mariot mariot added filigran team Item from the Filigran team. do not merge Do not merge until this label is removed. feature Type: new feature or capability (feat:). injector: email labels Jul 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new Email (SMTP) injector module to the OpenAEV injectors monorepo, including its runtime entrypoint, SMTP client, configuration schema, containerization, and initial tests/docs.

Changes:

  • Added a new email/ injector package (email_injector) with contract definition, config loader, and SMTP send implementation.
  • Added Docker build/run assets (Dockerfile, docker-compose.yml) plus sample configs (config.yml.sample, .env.sample).
  • Added initial tests and a README describing SMTP-specific configuration.

Reviewed changes

Copilot reviewed 16 out of 22 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
email/test/test_email_helper.py Adds a unit test for payload building.
email/test/test_email_client.py Adds unit tests for SMTP send behavior (mocked).
email/test/init.py Initializes the test package.
email/README.md Documents injector purpose and SMTP config keys.
email/pyproject.toml Defines the injector package + dependencies/dev tooling.
email/manifest-metadata.json Adds catalog/manager metadata for the injector image.
email/email-injector-findings.md Research/notes for future scope (signatures/headers/templates).
email/email_injector/openaev_email.py Injector entrypoint wiring config/contracts to SMTP send execution.
email/email_injector/helpers/email_helper.py Implements payload builder from inject content.
email/email_injector/helpers/init.py Package init for helpers.
email/email_injector/contracts_email.py Defines the Email inject contract (from/to/subject/body).
email/email_injector/configuration/injector_config_override.py Declares injector config schema including SMTP settings.
email/email_injector/configuration/config_loader.py Flattens OpenAEV/injector/SMTP config into daemon config.
email/email_injector/configuration/init.py Package init for configuration.
email/email_injector/client/email_client.py Implements SMTP send via smtplib.
email/email_injector/client/init.py Package init for client.
email/email_injector/init.py Package init for email injector.
email/Dockerfile Builds a container image for the email injector via Poetry.
email/docker-compose.yml Provides compose template to run the injector container.
email/config.yml.sample Provides a sample YAML config for manual deployment.
email/.env.sample Provides a sample env file for docker-compose deployment.

Comment thread email/test/test_email_client.py Outdated
Comment thread email/email_injector/client/email_client.py Outdated
Comment thread email/email_injector/configuration/config_loader.py
Comment thread email/docker-compose.yml
Comment thread email-smtp/.env.sample
Comment thread email/config.yml.sample
Comment thread email/README.md Outdated
Comment thread email/pyproject.toml Outdated
Comment thread email/pyproject.toml Outdated
Comment thread email/email_injector/openaev_email.py Outdated
@mariot

mariot commented Jul 7, 2026

Copy link
Copy Markdown
Member Author
image image image

@mariot mariot changed the title feature/20-create-smtp feat(email): create SMTP send injector (#23) Jul 7, 2026
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.47926% with 25 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ail-smtp/email_smtp/injector/openaev_email_smtp.py 83.33% 12 Missing ⚠️
email-smtp/email_smtp/contracts/email_contracts.py 72.72% 6 Missing ⚠️
...-smtp/email_smtp/contracts/craft_email/contract.py 68.75% 5 Missing ⚠️
...il-smtp/email_smtp/models/configs/config_loader.py 88.88% 1 Missing ⚠️
...il_smtp/models/configs/injector_config_override.py 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #289      +/-   ##
==========================================
+ Coverage   77.38%   77.82%   +0.43%     
==========================================
  Files         129      143      +14     
  Lines        5307     5524     +217     
==========================================
+ Hits         4107     4299     +192     
- Misses       1200     1225      +25     
Files with missing lines Coverage Δ
email-smtp/email_smtp/contracts/__init__.py 100.00% <100.00%> (ø)
...-smtp/email_smtp/contracts/craft_email/__init__.py 100.00% <100.00%> (ø)
email-smtp/email_smtp/injector/__init__.py 100.00% <100.00%> (ø)
email-smtp/email_smtp/models/__init__.py 100.00% <100.00%> (ø)
email-smtp/email_smtp/models/configs/__init__.py 100.00% <100.00%> (ø)
email-smtp/email_smtp/models/exceptions.py 100.00% <100.00%> (ø)
email-smtp/email_smtp/services/__init__.py 100.00% <100.00%> (ø)
email-smtp/email_smtp/services/email_client.py 100.00% <100.00%> (ø)
email-smtp/email_smtp/services/utils.py 100.00% <100.00%> (ø)
nuclei/nuclei/nuclei_contracts/nuclei_contracts.py 96.77% <ø> (ø)
... and 5 more

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mariot
mariot force-pushed the feature/20-create-smtp branch from 8818686 to 058b896 Compare July 7, 2026 09:59
@mariot
mariot requested a review from Megafredo July 8, 2026 06:13
@mariot
mariot force-pushed the feature/20-create-smtp branch from 63aacf9 to 058b896 Compare July 10, 2026 12:52

@Megafredo Megafredo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your work!

I performed several tests and successfully received the test emails. However, I identified a few points that could be improved:

  • It seems (to be confirmed) that the Dockerfile-based version is not working. This may be specific to my environment, but this point should be investigated.
  • The README.md is currently very concise. Several important pieces of information are missing, and it would benefit from being enriched to better align with our conventions.
  • As mentioned by Copilot, the tenant_id is missing from all available configurations.
  • A few remarks regarding conventions:
    • The default value for log_level should be error rather than info.
    • The folder should be named tests (plural).
  • Please note that the email-injector-findings file appears to be a development memo. (be deleted)
  • The PNG logo has a transparent background, which may cause display issues on the frontend, especially with dark themes.
Image
  • Regarding the log_level field, it would be preferable to use a Pydantic Literal in order to explicitly define the accepted log levels (debug, info, warning, error, critical) and ensure stricter configuration validation.

PS: One remark regarding the architecture: this is the point that sad me the most. The implementation was not based on the latest injector created, which is currently the reference for the injector guidelines (see CONTRIBUTING.md).

Future improvements

  • Currently, the configuration only supports user/password authentication. Would it be possible to consider adding support for other authentication methods, such as OAuth2 or certificate-based authentication?
  • At the moment, only the MIME format text/plain is supported. Supporting text/html could be an interesting improvement. Is limiting the support to plain text a deliberate design choice, or is HTML support something that could be considered in the future?
  • The feedback section Trace message could be improved. It would benefit from more detailed information?

@SamuelHassine SamuelHassine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full injector code (client, contracts, config loaders, helpers, Dockerfile, samples, tests), applied fixes directly on the branch and drove CI back to green. The SMTP client now uses a 30s timeout with a context-managed connection, the optional tenant id is propagated like in the other injectors, the README follows the standardized structure and the internal research notes were removed. All review threads are addressed and resolved, all checks pass.

@SamuelHassine

Copy link
Copy Markdown
Member

Review and fix pass summary:

  • Fixed (521b83d): SMTP connection now uses a 30s timeout and a context manager, so it is always closed even when STARTTLS/login/send fails; added a test asserting the connection is closed on send failure.
  • Fixed (521b83d): optional OpenAEV tenant id is now propagated (openaev_tenant_id in the config loader, OPENAEV_TENANT_ID in .env.sample and docker-compose.yml, commented tenant_id in config.yml.sample), matching the other injectors.
  • Fixed (521b83d): README extended to the standardized injector README structure (requirements, configuration tables, deployment incl. the required --build-context injector_common=../injector_common, contract fields, behavior, debugging).
  • Fixed (521b83d): removed the internal research notes file email-injector-findings.md from the injector directory.
  • Fixed (4dd75ca): restored the import order expected by the root-level isort --profile black CI check.
  • All 8 open review threads were addressed, replied to and resolved.
  • CI is fully green (GitHub Actions tests, CircleCI formatting/linter/tests, codecov, signed commits, CLA).
  • PR body updated with proposed changes, testing instructions (Mailpit) and checklist.

Remaining non-CI blockers for the maintainers:

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 21 changed files in this pull request and generated 1 comment.

Comment thread email-smtp/email_smtp/services/email_client.py

@SamuelHassine SamuelHassine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving after an independent senior review of the full changed files (not just the diff).

SMTP handling is solid: the connection uses a 30s timeout inside a context manager, so it is always closed on every failure path (STARTTLS / login / send); credentials are never logged; Bcc is kept out of the headers but included in the envelope recipient list; comma-separated Cc/Bcc parsing strips and drops empties; attachment download from OpenAEV is validated (status 200, non-null, binary). CRLF header injection in from/to/cc/subject is blocked by the standard-library email policy (it raises on embedded headers), which the client turns into a clean failure result - now covered by tests.

Two changes pushed in this pass (chore commit):

  • Added email/.dockerignore (config.yml, .env, pycache). It was the only buildable injector missing one, and the Dockerfile uses ADD . ${installdir}, so a local config.yml with real SMTP credentials or bytecode caches could otherwise be baked into the image.
  • Added unit tests locking the CRLF header-injection defense for the recipient and subject fields.

Leaving the "do not merge" label in place: this is the MVP1 base of a stack (#299 and #300 build on it) and merge order is handled by maintainers. Approval only - not merging.

@SamuelHassine

Copy link
Copy Markdown
Member

Review-and-fix pass summary (independent senior review + fixes):

  • SMTP security verified: 30s timeout + context manager guarantee the connection closes on every failure path; credentials are never logged; Bcc stays out of headers but is in the envelope recipient list; Cc/Bcc parsing strips and drops empties; attachment download is validated (status 200, non-null, binary). CRLF header injection in from/to/cc/subject is rejected by the stdlib email policy and surfaces as a clean error.
  • Added email/.dockerignore (config.yml, .env, pycache): it was the only buildable injector missing one, and the Dockerfile ADD . ${installdir} step could otherwise bake a local config.yml (real SMTP credentials) or caches into the image.
  • Added unit tests locking the CRLF header-injection defense (recipient + subject). Full suite: 15 tests pass. black / isort / flake8 clean.
  • Resolved the remaining Copilot thread about email/ shadowing the stdlib email package: it is a false positive. The directory has no __init__.py (PEP 420 namespace portion), and a regular package (stdlib email) always wins over a namespace portion; verified import email resolves to the stdlib and pytest email/test passes from the repo root.
  • No history rewrite: the check-signed-commits check is green for the whole branch, so every existing commit is properly signed (locally git log shows E for some only because the authors' signing keys are not in my keyring, not because they are unsigned). My change was therefore an additive, GPG-signed commit - no squash / force-push - so the feat(email): add independent from and mail from (#293) #299 / feat(email): add optional reply-to field to email injects (#294) #300 stack is unaffected.
  • CI: all 30 checks green. Mergeable: CLEAN.

Note: the "do not merge" label is intentional and left in place - this is the MVP1 base of a stack (#299, #300) and merge order is a maintainer decision. Approved only, not merged.

@SamuelHassine

Copy link
Copy Markdown
Member

Review and fix completed:

  • Independent review fixes: fail fast on incomplete SMTP credentials, validate attachment document_id values, correct the tenant configuration sample indentation, and remove the unnecessary contract type suppression.
  • Test coverage: added SMTP credential validation coverage and injector entrypoint tests for contract rejection, attachment handling, missing document_id, and execution callbacks.
  • Review threads: replied to and resolved all remaining Copilot threads. Unresolved thread count is 0.
  • Verification: local unittest and pytest passed; root black, isort, and flake8 checks passed. GitHub Actions, CircleCI, Codecov, signed-commit, linked-issue, and CLA checks are green.
  • PR state: mergeable and CLEAN. The intentional do not merge label remains in place; this PR was approved only and was not merged.

@mariot
mariot force-pushed the feature/20-create-smtp branch 4 times, most recently from 873d024 to 8cb30f8 Compare July 21, 2026 08:26
@Megafredo
Megafredo self-requested a review July 24, 2026 06:42

@Megafredo Megafredo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Thanks for the fixes and the architecture! Everything looks good to me.

mariot and others added 21 commits July 24, 2026 10:44
…23)

- add a 30s timeout to the SMTP connection and use a context manager so
  the connection is always closed, even when STARTTLS/login/send fails
- propagate the optional OpenAEV tenant id (openaev_tenant_id) through
  the config loader and add OPENAEV_TENANT_ID to .env.sample,
  docker-compose.yml and config.yml.sample, matching other injectors
- extend the README to the standardized injector README structure
  (requirements, configuration tables, deployment, contract fields,
  behavior, debugging)
- remove internal research notes (email-injector-findings.md) from the
  injector directory
- update tests for the context-managed SMTP client and add a test
  asserting the connection is closed when send fails
Co-authored-by: Samuel Hassine <samuel.hassine@filigran.io>
…#23)

- Add email/.dockerignore (config.yml, .env, __pycache__), matching every
  other buildable injector. Without it, `ADD . ${installdir}` in the Dockerfile
  could bake a local config.yml (real SMTP credentials) or bytecode caches into
  the published image.
- Add unit tests asserting the SMTP client fails closed on CRLF header
  injection in the recipient and subject fields (the stdlib email policy raises
  when a header value contains an embedded header), so this security property is
  regression-protected.
- rename dir email/ -> email-smtp/ and package email_injector -> email_smtp_injector
- rename entrypoint openaev_email.py -> openaev_email_smtp.py
- change injector type openaev_email -> openaev_email_smtp and image to openaev/injector-email-smtp
- rename contract and injector display name to "Email (SMTP)"
- update manifest, docker-compose, .env/config samples and README

Distinguishes the SMTP transport from the new Email (Microsoft 365) and
Email (Google Workspace) API-based email injectors.
…tandards (#23)

- convert test_email_client.py and test_email_helper.py from pytest-style
  functions to unittest.TestCase classes so they run under both
  python -m unittest (CircleCI runner) and pytest (GitHub Actions runner),
  matching the style of the other injectors (slack, teams, stratus,
  ai-redteam, injector_common)
- add the missing .gitignore (config.yml, .env, __pycache__), previously the
  only injector without one
- fix the stale contract label in the README ("Email - Craft email" ->
  "Email (SMTP) - Craft email") left over from the rename
- drop the redundant mandatory=False inside ContractBuilder.optional(), which
  already sets it
- align pyoaev pin to ==2.260717.0 and bump the injector version to
  2.260719.0, matching the repo-wide versions after merging main
Validate incomplete SMTP credentials and attachment identifiers, correct the tenant configuration sample, and cover injector execution callbacks.

Co-authored-by: Mariot Tsitoara <mariot.tsitoara@filigran.io>
@mariot
mariot force-pushed the feature/20-create-smtp branch from 8cb30f8 to ff55eb9 Compare July 24, 2026 08:44
@mariot
mariot marked this pull request as draft July 24, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do not merge Do not merge until this label is removed. feature Type: new feature or capability (feat:). filigran team Item from the Filigran team. injector: email

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(smtp): create SMTP/email send injector feat(smtp): adds attachments

5 participants