Skip to content

AttachmentDownloadService#379

Open
TatevikGr wants to merge 3 commits intodevfrom
feat/attachment
Open

AttachmentDownloadService#379
TatevikGr wants to merge 3 commits intodevfrom
feat/attachment

Conversation

@TatevikGr
Copy link
Contributor

@TatevikGr TatevikGr commented Feb 9, 2026

Summary by CodeRabbit

  • New Features

    • New attachment download service that validates files, detects MIME types, and returns downloadable content
    • Added a lightweight downloadable attachment DTO and a specific exception for missing attachment files
  • Bug Fixes

    • Attachment download URLs now use a path-based format (/ID/), updating generated links
  • Tests

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

Thanks for contributing to phpList!

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Warning

Rate limit exceeded

@TatevikGr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds attachment download support: new DownloadableAttachment DTO, AttachmentDownloadService that validates and reads files (with MIME detection), a new AttachmentFileNotFoundException, a URL format change for attachment links, updated tests, and composer dependency adjustments.

Changes

Cohort / File(s) Summary
Exception
src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php
New RuntimeException subclass used when an attachment file is missing or inaccessible; constructor provides a default message.
DTO
src/Domain/Messaging/Model/Dto/DownloadableAttachment.php
New readonly value object exposing filename, mimeType, size, and content (StreamInterface).
Download Service
src/Domain/Messaging/Service/AttachmentDownloadService.php
New service to resolve repository path, validate/sanitize filenames, check existence/readability, determine MIME type, open a stream and return a DownloadableAttachment; throws AttachmentFileNotFoundException on errors.
Attachment URL / Adder
src/Domain/Messaging/Service/AttachmentAdder.php
Changed generated download URL format from query parameter (?id=...&uid=...) to path-segment style (/ATTACHMENT_ID/?uid=...).
Tests: Service & Adder
tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php, tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php
New unit tests for AttachmentDownloadService covering error cases, MIME detection, and stream/content returns; updated AttachmentAdder test to expect the new URL format.
Composer
composer.json
Moved guzzlehttp/guzzle from require-dev into require (added to runtime dependencies).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Service as AttachmentDownloadService
  participant FS as Filesystem (repository)
  participant Mime as MimeTypes/Guesser
  participant Utils as Guzzle Utils
  participant DTO as DownloadableAttachment

  Client->>Service: request getDownloadable(Attachment)
  Service->>Service: sanitize filename (basename) and resolve path
  Service->>FS: check path safety, exists, readable
  alt file missing or unsafe
    FS-->>Service: error
    Service-->>Client: throw AttachmentFileNotFoundException
  else file ok
    Service->>Mime: ask for mime type (provided or guess from path)
    Mime-->>Service: mimeType
    Service->>FS: stat file (size) and open stream
    FS-->>Utils: create StreamInterface from file
    Utils-->>Service: StreamInterface
    Service->>DTO: instantiate DownloadableAttachment(filename, mimeType, size, stream)
    Service-->>Client: return DownloadableAttachment
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
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.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: introducing a new AttachmentDownloadService class with supporting exception and DTO classes.

✏️ 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*'

$hash = $forwarded ? 'forwarded' : $email->getTo()[0]->getAddress();
// todo: add endpoint in rest-api project
$viewUrl = $this->attachmentDownloadUrl . '/?id=' . $att->getId() . '&uid=' . $hash;
$viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . $hash;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider URL-encoding the uid query parameter value.

$hash can be an email address containing characters like + that have special meaning in query strings. Without urlencode(), a + would be interpreted as a space by the receiving end.

Suggested fix
-                    $viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . $hash;
+                    $viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . urlencode($hash);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . $hash;
$viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . urlencode($hash);
🤖 Prompt for AI Agents
In `@src/Domain/Messaging/Service/AttachmentAdder.php` at 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.

Comment on lines +40 to +41
$size = filesize($filePath);
$size = $size === false ? null : $size;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

$size can be null but DownloadableAttachment::$size is typed int.

This is the root cause of the type mismatch flagged on the DTO. Either coalesce here or fix the DTO type.

Quick fix: coalesce to 0
-        $size = $size === false ? null : $size;
+        $size = $size === false ? 0 : $size;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$size = filesize($filePath);
$size = $size === false ? null : $size;
$size = filesize($filePath);
$size = $size === false ? 0 : $size;
🤖 Prompt for AI Agents
In `@src/Domain/Messaging/Service/AttachmentDownloadService.php` around lines 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.

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