---
title: "Files"
description: "`files->list`, `listAll`, `iterate`, `get`, `download`, `listLinks`, `listAllLinks`, `iterateLinks`, `createLink`, `revokeLink`, `revokeAllLinks`, `stats`, `upload`, `delete` and `deleteMany`."
url: "https://openemail.uk/docs/php/files"
area: "PHP"
category: "Mailbox"
---

# Files

`files->list`, `listAll`, `iterate`, `get`, `download`, `listLinks`, `listAllLinks`, `iterateLinks`, `createLink`, `revokeLink`, `revokeAllLinks`, `stats`, `upload`, `delete` and `deleteMany`.

## Every method

**files.php**

```
use OpenEmail\Constants\FileKinds;
use OpenEmail\Constants\FileSorts;

$largest = $client->files->list(kind: FileKinds::PDF, sort: FileSorts::LARGEST, limit: 1);

foreach ($largest as $file) {
    foreach ($client->files->listAllLinks($file['id']) as $link) {
        echo $link['url'], ' ', $link['downloads'], PHP_EOL;
    }

    file_put_contents(basename($file['filename']), $client->files->download($file['id']));
}

$stats = $client->files->stats();
echo $stats['totals']['files'], ' ', $stats['uploaded']['bytes'], PHP_EOL;
```

Every file the mailbox holds, attachments sent and received and the files uploaded to it: the Files page of the app. `download` returns the bytes as a string, which `file_put_contents` saves unchanged. The links are the download links a file went out as, with how often each was fetched, and `stats` is the Analytics tab.

`list`, `listAll` and `iterate` take `q:`, `kind:` and `sort:`, and also `direction:` (`inbound`, `outbound` or `uploaded`), `address:`, the files of one address compared without regard to case, and `since:` and `until:`, a `DateTimeInterface` or an ISO 8601 string. `since:` includes its moment and `until:` stops before it. A `DateTimeInterface` is sent in UTC, and a string holding a bare date such as `2026-09-01` is read as midnight UTC.

`q:` searches the file name and its type. `kind:` is `image`, `pdf`, `audio`, `video` or `text`, and `sort:` is `newest`, the default, `oldest`, `largest` or `name`. `OpenEmail\Constants\FileKinds`, `FileSorts` and `FileDirections` name the choices. `list` returns one `OpenEmail\Result\Page`, and `listAll` returns every file in one array. `iterate` returns a `Generator` that yields one file at a time and fetches the next page only when the loop needs it.

> Reading needs `files:read`, and uploading, deleting and publishing or revoking links need `files:write`, which includes `files:read`. A key made before files had scopes of their own was given the matching ones. A key without the scope is refused with a 403 `insufficient_scope`, thrown as a `PermissionException` whose `isScopeMissing()` is true. A key limited to particular addresses or domains sees only the files that arrived at them, so it does not see files uploaded for the whole workspace.

> `download` holds the whole file in memory, so fetch very large files one at a time. Pass the name through `basename()` before writing it, as the sample does, because it came from whoever sent the file.

## Public links

**links.php**

```
use OpenEmail\Constants\FileDirections;

$uploads = $client->files->listAll(direction: FileDirections::UPLOADED, since: new \DateTimeImmutable('2026-09-01T00:00:00Z'));
$file = $uploads[0] ?? null;

if ($file !== null) {
    $link = $client->files->createLink($file['id'], domain: 'acme.com');
    echo $link['url'], PHP_EOL;

    $client->files->revokeLink($file['id'], $link['id']);
}
```

`createLink` publishes a file at a public download link that opens with no sign-in, and returns the link. The link lives on the files host of `domain:` when that domain has one, such as `files.acme.com`, else on the files host of the address the file belongs to, else on the API address. A file that came in or went out on a message is copied to public storage first, and a program or a script is refused with a 422 `file_unshareable`, thrown as a `ValidationException`.

> Each `createLink` makes a new link, so it is never retried after a lost answer: look for the link with `listLinks` first. `revokeLink` stops a link for good, including in mail that already went out, and returns it with `revokedAt` set. Revoking one twice is safe, so the client retries it like a read. An unknown link is a 404, thrown as a `NotFoundException`. `revokeAllLinks` stops every link of one file at once and reports how many in `revoked`.

## Upload and delete

**upload.php**

```
$report = "month,sent\n2026-09,1200\n";
$upload = $client->files->upload($report, filename: 'report.csv', contentType: 'text/csv');

if ($upload['deletable']) {
    $client->files->delete($upload['id']);
}

$logo = $client->files->upload(new \SplFileInfo('logo.svg'));
echo $logo['filename'], ' ', $logo['mimeType'], PHP_EOL;

$result = $client->files->deleteMany(['file_0c4e7a91d2b84f63a5e19b7d', 'file_6bb640f5b99e47deb758f1f5']);

foreach ($result['kept'] as $kept) {
    echo $kept['filename'], ' ', $kept['reason'], PHP_EOL;
}
```

`upload` stores the bytes as they are, up to 100 MB, under the name in `filename:`, and returns the file. A send attaches it as `['fileId' => $upload['id']]` in the `attachments` of `emails->send`. Pass `contentType:`, or data that carries its own type, and anything else is stored as `application/octet-stream`. The type is never guessed from `filename:`, so a string of bytes needs `contentType:` to be stored as anything else. It is never retried, because a second attempt would store a second copy.

> The data is a string of bytes, an `SplFileInfo`, a stream from `fopen()`, or a PSR-7 stream or uploaded file. An `SplFileInfo` or a stream opened on a file needs neither argument: the name is the file’s own, and the type comes from its extension when the package knows it, such as `.pdf` or `.png`. A Laravel or Symfony upload, and a PSR-7 uploaded file, bring the name and type the browser sent. A string carries no name, so a string without `filename:` throws an `InvalidArgumentException` before anything is sent.

> An upload may run for 600 seconds, or for the client’s `timeout` when that is longer, and `timeout:` sets another limit for one call. A client built with `timeout: 0` waits as long as an upload takes.

> Only an upload that nothing depends on can be deleted, and `deletable` and `usage` on each file say so ahead of time. `delete` refuses any other file with a 409 `file_in_use`, thrown as a `ConflictException`. `deleteMany` takes up to 100 ids, deletes what it can, and reports the rest in `kept`, each with its reason, and in `missing`. Neither is retried. A 404 on a second `delete` after a lost answer means the first one worked, and a second `deleteMany` reports those files in `missing`.
