migears / mail
Minimalist mail sending library with native mail() and SMTP support
Requires
- php: ^8.1
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A minimalist email sending library with zero required dependencies.
Background: miGears is the open-source successor of TinyGears, a self-developed PHP framework. It was renamed and open-sourced recently because the name TinyGears is already taken in the open-source community.
Features
- PHP 8.1+, using modern syntax (readonly, enums, type declarations)
- Zero required dependencies
- Immutable mail message value object
- Fluent chainable API
- Email address validation on set (blocks CRLF header injection)
- Supports HTML emails and attachments
- Two built-in sending drivers: native
mail()function and SMTP - SMTP transport can be injected for testing
- PSR-4 autoloading compliant
Boundaries
In scope
- The immutable
Mailvalue object and its fluentwith*API — sender, to/cc/bcc, reply-to, subject, HTML or plain body, charset, custom headers and file-path attachments; addresses are validated withfilter_varon set, which also blocks CRLF header injection. - The
MailerInterface::send(Mail): voidcontract and its two built-in drivers:NativeMailerover PHP'smail()andSmtpMailerspeaking SMTP over a socket/stream, plus the injectableTransport\SmtpTransportabstraction and its defaultSocketSmtpTransport. - Wire serialization owned by the drivers: headers and MIME parts, stripping CR/LF from user-supplied header values, escaping quoted MIME parameters, RFC 2047 base64-encoding of non-ASCII subjects and display names, RFC 2231 encoding of non-ASCII attachment filenames, the STARTTLS/SSL upgrade and
AUTH LOGIN; failures surface asMiGears\Mail\Exception\MailException.
Not in scope (by design)
- Queueing, retrying, scheduling or batching —
send()is synchronous: connect, deliver once, throw on failure, with no store-and-forward. Async execution belongs tomigears/jobs, which is itself explicitly queue-less. - Templating or rendering the body — no view engine, layout or partials; the caller supplies the final body string. Rendering belongs to
migears/template. - Receiving mail — sending only: no POP3/IMAP client, no mailbox reading, no inbound MIME parsing.
- MX/DNS resolution and any address book —
SmtpMailerconnects to the host you configure, and addresses are validated for RFC format only, not deliverability.
Installation
composer require migears/mail
Quick Start
use MiGears\Mail\Mail; use MiGears\Mail\NativeMailer; use MiGears\Mail\SmtpMailer; // Build the email $mail = (new Mail()) ->withFrom('sender@example.com', 'Sender Name') ->withTo('recipient@example.com') ->withCc('cc@example.com') ->withBcc('bcc@example.com') ->withSubject('Hello World') ->withBody('<p>This is an HTML email</p>', isHtml: true) ->withAttachment('/path/to/file.pdf', 'document.pdf', 'application/pdf'); // Send using native mail() $mailer = new NativeMailer(); $mailer->send($mail); // Send using SMTP $smtpMailer = new SmtpMailer( host: 'smtp.example.com', port: 587, username: 'user@example.com', password: 'secret', encryption: 'tls', ); $smtpMailer->send($mail);
API Reference
Mail (Immutable Value Object)
All with* methods return a new Mail instance; the original instance remains unchanged.
withFrom / withTo / withCc / withBcc / withReplyTo validate their email addresses (via filter_var) and throw MailException on invalid input. This also prevents CRLF header injection.
| Method | Description |
|---|---|
withFrom(string $email, string $name = '') |
Set the sender |
withTo(string ...$emails) |
Set recipients |
withCc(string ...$emails) |
Set CC recipients |
withBcc(string ...$emails) |
Set BCC recipients |
withReplyTo(string $email) |
Set reply-to address |
withSubject(string $subject) |
Set the subject |
withBody(string $body, bool $isHtml = false) |
Set the body |
withCharset(string $charset) |
Set the charset |
withHeaders(array $headers) |
Set custom headers |
withAttachment(string $path, ?string $name = null, ?string $type = null) |
Add an attachment |
hasAttachments(): bool |
Whether there are attachments |
getContentType(): string |
Get Content-Type |
getFormattedFrom(): string |
Get formatted sender |
MailerInterface
interface MailerInterface { public function send(Mail $mail): void; }
Built-in Implementations
- NativeMailer - Uses PHP's native
mail()function, supports attachments (multipart/mixed) - SmtpMailer - Implements SMTP protocol using
fsockopen, supports TLS/SSL and LOGIN authentication
Both drivers require a non-empty sender (from). If SmtpMailer is configured with encryption: 'tls' and the server does not advertise STARTTLS, it throws MailException rather than silently downgrading to plaintext. The encryption mode is case-insensitive and any value other than ''/tls/ssl is rejected at construction. Providing only a username or only a password also throws rather than silently skipping authentication.
Note that encryption: '' is an explicit opt-out: with credentials supplied, AUTH LOGIN is then sent in the clear. The no-silent-downgrade guarantee applies to the tls/ssl modes only — it is not a promise that credentials are encrypted on every configuration.
All user-supplied values that end up in message headers (display name, subject, custom header names/values, charset, attachment names and types) have CR/LF characters stripped at serialization time by the mailer driver, so no injected header line (e.g. Bcc:) can be smuggled in. Custom header names containing : are rejected. The Mail value object itself is wire-format agnostic and performs no CRLF filtering — header-safety is a driver responsibility. A custom header overrides a built-in one of the same name in both drivers, with one exception in the native driver — it builds From, Reply-To, Cc, Bcc, MIME-Version, Content-Type, Content-Transfer-Encoding and X-Mailer as headers, but hands the real recipients and subject to mail() as its own arguments rather than as headers, so a custom To/Subject header is appended alongside them and the delivered message carries both. The SMTP driver builds To and Subject itself, so it overrides them cleanly like any other built-in. A non-ASCII subject is RFC 2047 base64-encoded by both drivers; a non-ASCII display name is RFC 2047-encoded the same way and a non-ASCII attachment filename carries an RFC 2231 name*/filename* companion. Attachment name and filename values are escaped as quoted-strings.
The display name is additionally escaped (\ and ") by Mail::getFormattedFrom() before being wrapped in quotes, so it cannot break out of "..." and inject extra addresses into the From header; a non-ASCII display name is emitted as an RFC 2047 encoded-word instead.
The Mail constructor validates all email addresses (from, to, cc, bcc, replyTo) just like the with* methods, so new Mail(to: [...]) is just as safe as (new Mail())->withTo(...).
Driver differences
Because NativeMailer delegates to PHP's mail(), whose first argument always becomes the To: header, two capabilities differ from SmtpMailer:
- cc/bcc-only messages.
SmtpMaileraccepts a message with notorecipients (it delivers to thecc/bccenvelope).NativeMailerrejects it withNo recipient specified, sincemail()has no way to deliver to acc/bccaddress without also exposing it as theTo:header. Bccheader.SmtpMailerwrites no built-inBcc:header of its own (it only issuesRCPT TOfor those addresses); a customBccsupplied viawithHeaders()is written through verbatim like any other custom header.NativeMailerpassesBcc:in the headers and relies on the local MTA to strip it, which is the standardmail()practice.
For testing, SmtpMailer accepts an optional injected MiGears\Mail\Transport\SmtpTransport (see the SocketSmtpTransport default implementation); NativeMailer exposes a protected deliver() seam over mail().
Exceptions
All sending failures throw MiGears\Mail\Exception\MailException.
Testing
composer install vendor/bin/phpunit
Tests use InMemoryMailer (in-memory implementation) for assertions, no real mail server required.
License
MIT
migears/mail
极简邮件发送库,零强制依赖。
特性
- PHP 8.1+,使用现代语法(readonly、枚举、类型声明)
- 零强制依赖
- 不可变邮件消息值对象
- 流畅的链式调用 API
- 设置地址时校验邮箱格式(阻断 CRLF 头注入)
- 支持 HTML 邮件和附件
- 内置两种发送驱动:原生
mail()函数和 SMTP - SMTP 传输层可注入以便测试
- 符合 PSR-4 自动加载规范
边界
范围内
- 不可变的
Mail值对象及其链式with*API —— 发件人、to/cc/bcc、reply-to、主题、HTML 或纯文本正文、charset、自定义头以及基于文件路径的附件;地址在设置时用filter_var校验,可阻断 CRLF 头注入。 MailerInterface::send(Mail): void契约及两个内置驱动:基于 PHPmail()的NativeMailer,以及通过 socket/stream 讲 SMTP 的SmtpMailer;同时提供可注入的Transport\SmtpTransport抽象及其默认实现SocketSmtpTransport。- 由驱动负责的报文序列化:组装邮件头与 MIME 分部、剥离用户输入头值中的 CR/LF、转义带引号的 MIME 参数、对非 ASCII 主题与显示名做 RFC 2047 base64 编码、对非 ASCII 附件名做 RFC 2231 编码、STARTTLS/SSL 升级与
AUTH LOGIN;所有失败统一抛出MiGears\Mail\Exception\MailException。
范围外(刻意不做)
- 排队、重试、调度或批量发送 ——
send()是同步的:连接、投递一次、失败即抛,不做存储转发。异步执行属于migears/jobs,而后者自身也明确不做队列。 - 模板渲染正文 —— 不含视图引擎、布局或分部;正文由调用方传入最终字符串。渲染属于
migears/template。 - 收信 —— 只负责发送:没有 POP3/IMAP 客户端、不读信箱,也不解析入站邮件的 MIME。
- MX/DNS 解析与通讯录 ——
SmtpMailer只连接你配置的主机;地址仅按 RFC 格式校验,不校验可达性。
安装
composer require migears/mail
快速开始
use MiGears\Mail\Mail; use MiGears\Mail\NativeMailer; use MiGears\Mail\SmtpMailer; // 构建邮件 $mail = (new Mail()) ->withFrom('sender@example.com', 'Sender Name') ->withTo('recipient@example.com') ->withCc('cc@example.com') ->withBcc('bcc@example.com') ->withSubject('Hello World') ->withBody('<p>This is an HTML email</p>', isHtml: true) ->withAttachment('/path/to/file.pdf', 'document.pdf', 'application/pdf'); // 使用原生 mail() 发送 $mailer = new NativeMailer(); $mailer->send($mail); // 使用 SMTP 发送 $smtpMailer = new SmtpMailer( host: 'smtp.example.com', port: 587, username: 'user@example.com', password: 'secret', encryption: 'tls', ); $smtpMailer->send($mail);
API 参考
Mail (不可变值对象)
所有 with* 方法返回新的 Mail 实例,原实例保持不变。
withFrom / withTo / withCc / withBcc / withReplyTo 会(通过 filter_var)校验邮箱地址,非法输入抛 MailException;同时可阻断 CRLF 头注入。
| 方法 | 说明 |
|---|---|
withFrom(string $email, string $name = '') |
设置发件人 |
withTo(string ...$emails) |
设置收件人 |
withCc(string ...$emails) |
设置抄送 |
withBcc(string ...$emails) |
设置密送 |
withReplyTo(string $email) |
设置回复地址 |
withSubject(string $subject) |
设置主题 |
withBody(string $body, bool $isHtml = false) |
设置正文 |
withCharset(string $charset) |
设置字符集 |
withHeaders(array $headers) |
设置自定义头 |
withAttachment(string $path, ?string $name = null, ?string $type = null) |
添加附件 |
hasAttachments(): bool |
是否有附件 |
getContentType(): string |
获取 Content-Type |
getFormattedFrom(): string |
获取格式化的发件人 |
MailerInterface
interface MailerInterface { public function send(Mail $mail): void; }
内置实现
- NativeMailer - 使用 PHP 原生
mail()函数,支持附件(multipart/mixed) - SmtpMailer - 使用
fsockopen实现 SMTP 协议,支持 TLS/SSL 和 LOGIN 认证
两个驱动都要求非空发件人(from)。若 SmtpMailer 配置了 encryption: 'tls' 但服务端未宣告 STARTTLS,会抛出 MailException 而非静默降级为明文。加密模式大小写不敏感,构造时仅接受 ''/tls/ssl,其他值直接抛异常。只提供用户名或只提供密码也会抛异常,而不会静默跳过认证。
需要注意:encryption: '' 是显式的「不加密」选择——此时若提供了凭据,AUTH LOGIN 会以明文发送。「不静默降级」的保证只适用于 tls/ssl 模式,并不等于「任何配置下凭据都加密」。
所有会进入邮件头的用户输入(显示名、主题、自定义头名与值、charset、附件名与类型)在序列化时由 Mailer 驱动剥离 CR/LF 字符,因此无法注入额外的头部行(如 Bcc:)。含冒号的自定义头名会被拒绝。Mail 值对象本身与传输格式无关,不做 CRLF 过滤——头部安全是驱动层的职责。两个驱动中同名自定义头都会覆盖同名内置头,但原生驱动有一个例外:它构建的头是 From、Reply-To、Cc、Bcc、MIME-Version、Content-Type、Content-Transfer-Encoding 与 X-Mailer,而真实的收件人与主题是作为 mail() 自身的参数传入、而非作为头构建的,因此自定义 To/Subject 头会被追加在其旁边,投递出的报文同时带有两份。SMTP 驱动自身构建 To 与 Subject,因此与其他内置头一样被干净覆盖。非 ASCII 主题与显示名按同样规则做 RFC 2047 编码,非 ASCII 附件名附带 RFC 2231 的 name*/filename*。附件的 name 与 filename 按带引号字符串转义。
ASCII 显示名的 \ 与 " 转义由 Mail::getFormattedFrom() 完成(属格式化职责),因此无法突破 "..." 向 From 头注入额外地址;非 ASCII 显示名则改为 RFC 2047 编码字。
Mail 构造器与 with* 方法一样会校验全部邮箱地址(from、to、cc、bcc、replyTo),因此 new Mail(to: [...]) 与 (new Mail())->withTo(...) 同样安全。
驱动差异
由于 NativeMailer 依赖 PHP 的 mail(),而 mail() 的第一个参数总会成为 To: 头,因此有两处能力与 SmtpMailer 不同:
- 仅 cc/bcc 的邮件。
SmtpMailer接受没有to收件人的邮件(通过 cc/bcc 信封投递)。NativeMailer会以No recipient specified拒绝,因为mail()无法在不让 cc/bcc 地址暴露为To:头的前提下投递给它们。 Bcc头。SmtpMailer本身不写内置Bcc:头(只为这些地址发RCPT TO);经withHeaders()传入的自定义Bcc与其他自定义头一样原样写出。NativeMailer会把Bcc:放进头部,依赖本地 MTA 剥离——这是mail()的标准做法。
为便于测试,SmtpMailer 接受可选注入的 MiGears\Mail\Transport\SmtpTransport(默认实现为 SocketSmtpTransport);NativeMailer 则暴露了一个包装 mail() 的 protected deliver() 接缝。
异常
所有发送失败抛出 MiGears\Mail\Exception\MailException。
测试
composer install vendor/bin/phpunit
测试中使用 InMemoryMailer(内存实现)进行断言,无需真实邮件服务器。
License
MIT