sdjgroup / mail-proxy-php
Framework-agnostic PHP SDK for the mail proxy service: hand a mail with many recipients and attachments to the proxy in a few lines, with typed errors and zero dependencies.
Requires
- php: >=8.2
- ext-curl: *
- ext-json: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^1.12 || ^2.0
- phpunit/phpunit: ^11.5.50
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-13 07:42:57 UTC
README
Framework-agnostic PHP SDK for the mail proxy service. Hand a mail — with several recipients and attachments — to the proxy in a few lines; the service stores it, splits it per recipient and delivers it over SMTP.
中文文档见 README.zh-CN.md。
Why this package
- A mail in a few lines — a fluent builder, or a plain
MailMessageobject. - Many recipients, many attachments — the proxy stores one queue row per recipient, so every person is tracked and delivered separately.
- Attachments handled correctly — files are uploaded as
multipart/form-datawith the field naming the service requires; bytes held in memory are materialised into a temporary file and cleaned up afterwards, whatever the outcome. - Typed errors — every failure is a
MailException; business errors carry the original code, the HTTP status and aMailErrorenum case. - Fails fast, locally — recipient, attachment count and attachment size limits are checked before anything is uploaded.
- Zero dependencies — no Guzzle, no PSR packages. Only
ext-curlandext-json. - Framework agnostic — plain PHP, Laravel, ThinkPHP, Symfony, Hyperf… the transport is a replaceable interface.
Requirements
| Requirement | Version |
|---|---|
| PHP | >= 8.2 |
| ext-curl | any |
| ext-json | any |
Installation
composer require sdjgroup/mail-proxy-php
Quick start
use Sdjgroup\MailProxy\MailProxyClient;
$client = MailProxyClient::create(
host: 'http://192.168.57.69:8083',
sourceFrom: 'crm', // how the service tells callers apart
);
$result = $client->mail()
->from('default_sender') // a sender `key` registered in the proxy
->subject('月度账单')
->content('您好,附件是本月账单,请查收。')
->to('zhangsan@example.com', '张三')
->to('lisi@example.com')
->attachFile('/path/to/账单.pdf')
->send();
echo $result->total; // 2 — one queue row per recipient
print_r($result->ids()); // [128, 129] — their scheduled_mails ids
The endpoint is unauthenticated by design: services call services, and source_from is how
the proxy (and its logs) tells them apart. Set it once on the client, or override it per
mail with ->sourceFrom('order-service').
Recipients
Every recipient becomes its own queue row with its own id, so a mail to three people is three independently tracked deliveries.
$client->mail()
->to('zhangsan@example.com', '张三') // email required, name optional
->to('lisi@example.com')
->toMany(new Recipient('wangwu@example.com', '王五'));
The proxy de-duplicates identical addresses (A@x.com and a@x.com count as one), and
accepts at most 200 recipients per call.
Attachments
use Sdjgroup\MailProxy\Attachment;
$client->mail()
// … sender, subject, content, recipients …
->attachFile('/path/to/账单.pdf') // file on disk
->attachFile('/path/to/明细.xlsx', '十月明细.xlsx') // renamed
->attach(Attachment::fromContents($pdfBytes, '报表.pdf', 'application/pdf'));
Notes:
- The proxy never stores the attachment bytes in its database — only the name, size and md5. The bytes are written to the proxy's own storage and deleted once the mail has been delivered.
- Limits per call: 20 attachments, 20MB each, 50MB in total.
- Fields are named
attachments[0],attachments[1], … The SDK does this for you, and it matters: the service parses multipart itself and does not expand PHP's array syntax, soattachments[]would silently keep only the last file.
Scheduled sending
$client->mail()
// …
->scheduledAt(1750000000) // Unix timestamp
->at(new DateTimeImmutable('2026-10-13 09:00')) // …or a date
->send();
Omitting both sends as soon as the delivery process picks the mail up.
Proxies
The mail proxy usually lives on a private network, so the transport never uses a forward proxy unless you configure one.
That default is deliberate. libcurl silently honours the http_proxy and https_proxy
environment variables, so on a machine that has them set — a corporate workstation, a CI
runner — a client that does nothing about it would send the call into that proxy, where it
fails with a connection error that says nothing about the real cause. An explicit opt-out is
what makes the behaviour the same everywhere.
If the proxy really has to be reached through a forward proxy, configure it:
$config = new Config(
host: 'http://192.168.57.69:8083',
sourceFrom: 'crm',
proxy: 'http://10.0.0.1:3128',
);
$client = new MailProxyClient($config);
The same option is proxy in Config::fromArray(). If you replace the transport yourself,
this is your responsibility — see Custom transport.
Error handling
use Sdjgroup\MailProxy\Exception\MailApiException;
use Sdjgroup\MailProxy\Exception\MailException;
use Sdjgroup\MailProxy\MailError;
try {
$client->mail()->/* … */->send();
} catch (MailApiException $e) {
// The proxy answered with a business error.
$e->getErrorCode(); // 45002
$e->getMessage(); // "发件人已停用:marketing"
$e->error(); // MailError::SenderUnavailable
$e->isRetryable(); // false
} catch (MailException $e) {
// MailValidationException (bad input, nothing was sent) or
// MailHttpException (the request never got through).
}
| Exception | Raised when |
|---|---|
MailValidationException | The message breaks the contract; nothing was uploaded. getErrors() lists every problem. |
MailHttpException | DNS, connection, timeout or TLS failure. Nothing reached the proxy. |
MailApiException | The proxy rejected the request; carries the business code and MailError case. |
| Code | Meaning |
|---|---|
42200 | Parameter validation failed |
45002 | Sender does not exist or is disabled |
45003 | Too many recipients |
45004 | Too many attachments, or one is too large |
45005 | The proxy could not store an attachment |
50000 | Unexpected server-side failure (retryable) |
Custom transport
HttpClientInterface is the only extension point:
$client = new MailProxyClient(new Config($host, 'crm'), $myTransport);
Keep in mind that a transport must send multipart/form-data and must pass field names
through verbatim — see the interface docblock.
Testing
composer test # unit tests, no network
composer analyse # PHPStan level 8
composer cs-check # coding standard
composer check # all of the above
The integration suite talks to a real deployment and is skipped unless configured:
MAIL_PROXY_HOST=http://192.168.57.69:8083 \
MAIL_PROXY_SOURCE=crm \
MAIL_PROXY_SENDER_KEY=default_sender \
MAIL_PROXY_TO=you@example.com \
vendor/bin/phpunit --testsuite Integration
Note that this really queues a mail which the delivery process will send.
License
MIT — see LICENSE.