Files
`files->list`, `listAll`, `iterate`, `get`, `download`, `listLinks`, `listAllLinks`, `iterateLinks`, `createLink`, `revokeLink`, `revokeAllLinks`, `stats`, `upload`, `delete` and `deleteMany`.
Every method
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
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
$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.