Search by

sdjgroup / mail-proxy-php

lushaoming

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.

v1.0.0 2026-10-12 09:24 UTC

This package is not auto-updated.

Last update: 2026-10-13 07:42:57 UTC


README

Latest Version on Packagist License

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 MailMessage object.
  • 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-data with 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 a MailError enum 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-curl and ext-json.
  • Framework agnostic — plain PHP, Laravel, ThinkPHP, Symfony, Hyperf… the transport is a replaceable interface.

Requirements

RequirementVersion
PHP>= 8.2
ext-curlany
ext-jsonany

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, so attachments[] 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).
}
ExceptionRaised when
MailValidationExceptionThe message breaks the contract; nothing was uploaded. getErrors() lists every problem.
MailHttpExceptionDNS, connection, timeout or TLS failure. Nothing reached the proxy.
MailApiExceptionThe proxy rejected the request; carries the business code and MailError case.
CodeMeaning
42200Parameter validation failed
45002Sender does not exist or is disabled
45003Too many recipients
45004Too many attachments, or one is too large
45005The proxy could not store an attachment
50000Unexpected 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.