⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@TatevikGr
Copy link
Contributor

@TatevikGr TatevikGr commented Feb 9, 2026

Summary by CodeRabbit

  • New Features

    • Attachment download service that validates access, resolves files, detects MIME types, and returns downloadable content.
    • Lightweight downloadable attachment DTO and a new exception for missing attachment files.
    • Public constant to mark forwarded attachments.
  • Bug Fixes

    • Attachment download links now use a path-based format with encoded UID.
  • Tests

    • New unit tests covering download behavior, MIME detection, stream content, and error cases.

Thanks for contributing to phpList!

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds attachment download support: new DownloadableAttachment DTO, AttachmentDownloadService with UID and path/file validation, AttachmentFileNotFoundException, Attachment::FORWARD constant and updated AttachmentAdder URL format, tests, and composer dependency change.

Changes

Cohort / File(s) Summary
Exception
src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php
New runtime exception for missing or unreadable attachment files.
DTO
src/Domain/Messaging/Model/Dto/DownloadableAttachment.php
New readonly DTO exposing filename, mimeType, size, and content (StreamInterface).
Download Service
src/Domain/Messaging/Service/AttachmentDownloadService.php
New service that validates UID (FORWARD bypass), sanitizes filenames, resolves repository path, checks existence/readability, determines MIME type, opens a stream and returns DownloadableAttachment; throws AttachmentFileNotFoundException or MessageNotReceivedException.
Attachment model
src/Domain/Messaging/Model/Attachment.php
Added public constant FORWARD = 'forwarded'.
Attachment URL / Adder
src/Domain/Messaging/Service/AttachmentAdder.php
Changed generated download URL from ?id=...&uid=... to /{id}/?uid=... and switched forwarded hash use to Attachment::FORWARD.
Tests
tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php, tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php
Added tests for AttachmentDownloadService (error cases, explicit/guessed MIME, stream content) and updated AttachmentAdder test to expect new URL format.
Composer
composer.json
Moved guzzlehttp/guzzle from require-dev into require (now a runtime dependency).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Service as AttachmentDownloadService
  participant Repo as SubscriberRepository
  participant FS as Filesystem
  participant Utils as GuzzleUtils
  participant DTO as DownloadableAttachment

  Client->>Service: getDownloadable(attachment, uid)
  Service->>Repo: validateUid(uid)
  alt uid == FORWARD or subscriber exists
    Repo-->>Service: OK
    Service->>Service: sanitize filename (basename)
    Service->>FS: resolve base repo path & realpath
    FS-->>Service: realpath result
    Service->>FS: check file exists & readable
    alt file missing/unreadable
      FS-->>Service: error
      Service-->>Client: throw AttachmentFileNotFoundException
    else file ok
      Service->>Service: determine mimeType (provided or guess)
      Service->>FS: stat file (size) and open handle
      Service->>Utils: create StreamInterface from handle
      Utils-->>Service: StreamInterface
      Service->>DTO: new DownloadableAttachment(filename, mimeType, size, stream)
      Service-->>Client: return DownloadableAttachment
    end
  else
    Repo-->>Service: not found
    Service-->>Client: throw MessageNotReceivedException
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Feat/email forwarding #377: Modifies messaging attachment handling and forwarded-UID logic — directly related to the FORWARD constant and URL changes.
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'AttachmentDownloadService' is vague and only names a single class being added, without clearly summarizing the main change or intent of the pull request. Consider a more descriptive title that captures the primary purpose, such as 'Add attachment download service with file validation' or 'Implement secure attachment downloading with path validation'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/attachment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/Domain/Messaging/Model/Dto/DownloadableAttachment.php`:
- Line 14: The DTO declares public readonly int $size but
AttachmentDownloadService may pass null (filesize() can return false); update
the DownloadableAttachment definition to use public readonly ?int $size so it
accepts null, and adjust the constructor/signature and any callers (e.g.,
wherever new DownloadableAttachment(...) is invoked) to handle nullable size;
alternatively, if you prefer non-null sizes, coalesce the service value to 0
when filesize() returns false in AttachmentDownloadService instead of passing
null.

In `@src/Domain/Messaging/Service/AttachmentAdder.php`:
- Line 61: The constructed download URL in AttachmentAdder (variable $viewUrl)
concatenates $hash directly into the uid query parameter which can corrupt
values like emails containing '+'; update the URL construction in the
AttachmentAdder.php code that builds $viewUrl (uses $this->attachmentDownloadUrl
and $att->getId()) to URL-encode the uid value (e.g., use urlencode on $hash) so
the uid query parameter is safely transmitted.

In `@src/Domain/Messaging/Service/AttachmentDownloadService.php`:
- Around line 40-41: The filesize() call can return false, but
DownloadableAttachment::$size is typed int, so coalesce or convert the result
before assigning to the DTO; in AttachmentDownloadService replace the current
$size assignment (the $size = filesize($filePath); $size = $size === false ?
null : $size;) with a safe int value (e.g. coalesce to 0 or cast to int) so
DownloadableAttachment::$size always receives an int, or alternatively change
DownloadableAttachment::$size to ?int if nulls are intended.
- Around line 29-34: The AttachmentDownloadService currently concatenates
$filename (from $attachment->getFilename()) onto $this->attachmentRepositoryPath
allowing path traversal; fix by resolving and validating the target path before
allowing access: compute the repository base path from
$this->attachmentRepositoryPath, sanitize or canonicalize the user filename
(e.g., use basename or join then run realpath on the combined path), verify the
resolved path starts with the repository base (preventing escapes like ../../),
and only then call is_file/is_readable and return or throw
AttachmentFileNotFoundException if validation fails; update the code around the
$path/$filePath construction in AttachmentDownloadService to implement these
checks.
- Line 7: The project imports GuzzleHttp\Psr7\Utils in AttachmentDownloadService
but guzzlehttp/guzzle is only listed in composer.json require-dev; move the
guzzlehttp/guzzle package from "require-dev" to "require" in composer.json so it
is installed in production, then run composer update (or composer install) to
ensure the dependency is available at runtime for the AttachmentDownloadService
class that uses Utils.
🧹 Nitpick comments (2)
src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php (1)

9-12: Consider accepting context for easier debugging.

The hardcoded message with no parameters makes it harder to trace which attachment failed. Accepting an optional attachment ID or filename would help with troubleshooting in production.

Suggested improvement
-    public function __construct()
+    public function __construct(string $detail = '')
     {
-        parent::__construct('Attachment file not available');
+        $message = 'Attachment file not available';
+        if ($detail !== '') {
+            $message .= ': ' . $detail;
+        }
+        parent::__construct($message);
     }
src/Domain/Messaging/Service/AttachmentDownloadService.php (1)

22-52: No unit tests for AttachmentDownloadService.

This is a new service with non-trivial logic (path resolution, MIME detection, stream creation, error handling). Consider adding tests covering at least: empty filename, missing file, successful download, and path traversal rejection.

#!/bin/bash
# Check if any test file exists for AttachmentDownloadService
rg -l "AttachmentDownloadService" --type=php -g '*Test*'

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/Domain/Messaging/Service/AttachmentDownloadService.php`:
- Around line 47-55: The code validates the path into $realPath but continues
using $filePath for downstream file ops; change all uses after validation to
$realPath so operations are on the canonical path—specifically use $realPath
when calling MimeTypes::getDefault()->guessMimeType(...), when calling
filesize(...), and when passing to Utils::tryFopen(...) / Utils::streamFor(...),
while keeping the $attachment->getMimeType() fallback logic unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/Domain/Messaging/Service/AttachmentDownloadService.php`:
- Around line 51-59: validateUid currently only verifies the UID belongs to an
existing subscriber (using subscriberRepository->findOneByEmail) but throws
MessageNotReceivedException, so it must also verify the subscriber actually
received the specific campaign/attachment; update validateUid to, when $uid !==
Attachment::FORWARD, lookup the subscriber via
subscriberRepository->findOneByEmail and then check message/recipient delivery
(e.g. via a MessageRepository or Recipient/Delivery entity lookup) that the
subscriber is a recipient of the campaign/attachment being requested, and only
return success if that delivery record exists; if no delivery record is found,
throw MessageNotReceivedException; alternatively, if the broader acceptance of
any subscriber is intended, add a clear TODO comment in validateUid noting this
design decision and why it omits the delivery check.
- Around line 29-31: The call to basename() currently receives
$attachment->getFilename() which is nullable; update AttachmentDownloadService
(around where $original is set) to guard against null before calling basename —
e.g., retrieve $original = $attachment->getFilename() and if null either
throw/return early or use a safe default via null-coalescing prior to $filename
= basename($original); then pass the safe $original/$filename into
validateFilePath($filename, $original) so basename() never receives null.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants