coffeemail / coffeemail-laravel
Driver oficial do CoffeeMail para Laravel Mailer, notificações e webhooks, com integração Symfony Mailer e suporte nativo a filas assíncronas.
Requires
- php: ^8.2
- coffeemail/coffeemail-php: ^0.1.0 || 0.1.x-dev || dev-main
- illuminate/mail: ^10.0 || ^11.0 || ^12.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0
- symfony/mailer: ^6.2 || ^7.0
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- pestphp/pest: ^2.34 || ^3.0
- phpstan/phpstan: ^1.10 || ^2.0
- squizlabs/php_codesniffer: ^3.10
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
☕ CoffeeMail Laravel
Integração oficial do CoffeeMail com o Laravel: mail transport para Mail/Mailables, facade sobre o SDK PHP e middleware de webhooks.
Instalação · Início rápido · Configuração · Referência · Webhooks · Status
📑 Sumário
- ✨ Visão geral
- 🚦 Status do projeto
- 📋 Requisitos
- 📦 Instalação
- 🚀 Início rápido
- ⚙️ Configuração
- 🛡️ Tratamento de erros
- 📚 Referência
- 🔔 Webhooks
- 🔖 Versionamento e releases
- 🧪 Testes e qualidade
- 🗂️ Estrutura do repositório
- 🤝 Contribuição
- 🔗 Projetos relacionados
- 📄 Licença
✨ Visão geral
Pacote Composer coffeemail/coffeemail-laravel, construído sobre o SDK PHP coffeemail/coffeemail-php. Entrega três peças:
| Peça | Classe | O que faz |
|---|---|---|
| Mail transport | CoffeeMail\Laravel\Transport\CoffeeMailTransport |
Transport do Symfony Mailer registrado no MailManager como coffeemail. Converte o Symfony\Component\Mime\Email montado pelo Laravel em EmailPayload e chama emails->send() do SDK (POST /v1/product/emails). |
| Facade | CoffeeMail\Laravel\Facades\CoffeeMail |
Acesso ao client CoffeeMail\CoffeeMail do SDK, registrado como singleton no container (alias coffeemail). |
| Webhooks | CoffeeMail\Laravel\Http\Middleware\VerifyWebhookSignature + CoffeeMail\Laravel\Events\WebhookReceived |
Middleware coffeemail.webhook que valida a assinatura x-webhook-* da API e dispara um evento Laravel. |
Fluxo de envio:
flowchart LR
A["Mail::to()->send(Mailable)<br/>Mail::raw() / Notification"] --> B[MailManager<br/>mailer 'coffeemail']
B --> C[CoffeeMailTransport::doSend]
C -->|EmailPayload| D["SDK PHP<br/>$client->emails->send()"]
D -->|"POST /v1/product/emails<br/>Bearer API key"| E[(API CoffeeMail)]
E -->|"{ data: { id } }"| C
C -->|setMessageId(id)| B
E -.->|erro| F[TransportException]
Loading
O ServiceProvider (CoffeeMail\Laravel\CoffeeMailServiceProvider) e o alias CoffeeMail são registrados por package auto-discovery (extra.laravel no composer.json).
🚦 Status do projeto
- Publicado no Packagist como
coffeemail/coffeemail-laravelv0.1.0 (2026-10-08), gerado a partir do commit79f1cb8. Amainestá à frente da tag: arquivoLICENSE, PHPStan nível 9, gatecheck:standardse ajuste interno no transport ainda não foram para uma release. - Pré-1.0. A superfície pública pode receber adições; ver Versionamento.
- A
0.2.0(emmain, não publicada) corrige o middleware de webhooks e exige o SDK PHP0.2.0. Nav0.1.0, o middleware liax-coffeemail-signaturee comparava o HMAC do corpo puro, então rejeitava toda entrega real. Veja o CHANGELOG. - Payload do evento: a API envia
{ "event_type", "webhook_id", "data" }; não existe chaveevent. tagsemetadatade Mailables não são mapeados para o campotagsdo CoffeeMail. O Laravel os transforma em cabeçalhosX-Tag/X-Metadata-<chave>, que o transport repassa como cabeçalhos customizados; com várias tags, só a última sobrevive (as chaves deheaderscolidem).- Base URL fixa. O SDK PHP aponta para
https://api.coffeemail.com.brsem opção de override, e o timeout é o default do SDK (10 s); nenhum dos dois é configurável por este pacote. - Dependência do SDK.
composer.jsonaceita^0.2.0 || 0.2.x-dev || dev-main. Enquanto o SDK PHP0.2.0não for publicado, amaindeste pacote só instala com o SDK emdev-main(repositório VCS oupath). - Sem fake próprio. Não há
CoffeeMail::fake(); nos testes da sua app useMail::fake()do Laravel.
📋 Requisitos
| Item | Versão (de composer.json) |
|---|---|
| PHP | ^8.2 (CI roda 8.2, 8.3 e 8.4) |
illuminate/mail, illuminate/support |
^10.0 || ^11.0 || ^12.0 |
symfony/mailer |
^6.2 || ^7.0 |
coffeemail/coffeemail-php |
^0.2.0 || 0.2.x-dev || dev-main |
| Extensões | ext-curl e ext-json (exigidas pelo SDK PHP) |
Uma API key da organização (cm_live_...), criada no dashboard do CoffeeMail, e um domínio remetente verificado.
📦 Instalação
composer require coffeemail/coffeemail-laravel
Publicar o arquivo de configuração (opcional; sem ele o pacote usa o config/coffeemail.php interno via mergeConfigFrom):
php artisan vendor:publish --tag="coffeemail-config"
🚀 Início rápido
.env:
MAIL_MAILER=coffeemail MAIL_FROM_ADDRESS=nao-responda@seudominio.com.br MAIL_FROM_NAME="Minha Loja" COFFEEMAIL_API_KEY=cm_live_sua_chave
config/mail.php, dentro demailers:
'coffeemail' => [ 'transport' => 'coffeemail', ],
- Enviar:
use Illuminate\Support\Facades\Mail; Mail::raw('Seu código de verificação é 849201.', function ($message): void { $message->to('cliente@gmail.com', 'João da Silva') ->subject('Código de verificação'); });
O id devolvido pela API vira o Message-ID do SentMessage retornado pelo Laravel.
⚙️ Configuração
config/coffeemail.php
| Chave | Env var | Tipo | Default | Uso |
|---|---|---|---|---|
api_key |
COFFEEMAIL_API_KEY |
string|null |
null |
API key usada pelo singleton da facade e, por padrão, pelo mailer. Se vazia, o SDK ainda tenta getenv('COFFEEMAIL_API_KEY'); sem nenhuma, o construtor do client lança CoffeeMail\Exceptions\AuthenticationError. |
locale |
COFFEEMAIL_LOCALE |
string |
'pt-BR' |
Enviado como Accept-Language e usado nas mensagens de erro do SDK. Suportados pelo SDK: pt-BR, en. |
webhook_secret |
COFFEEMAIL_WEBHOOK_SECRET |
string|null |
null |
Segredo (whsec_...) do middleware coffeemail.webhook. Sem ele, o middleware responde 500. |
webhook_tolerance |
COFFEEMAIL_WEBHOOK_TOLERANCE |
int |
300 |
Janela, em segundos, aceita entre x-webhook-timestamp e o relógio local. Fora dela, 401 (proteção contra replay). |
Mailer em config/mail.php
| Chave | Obrigatória | Default | Uso |
|---|---|---|---|
transport |
sim | — | Deve ser 'coffeemail'. |
api_key |
não | coffeemail.api_key |
Sobrescreve a API key só para este mailer (ex.: uma segunda organização). |
locale |
não | coffeemail.locale |
Sobrescreve o locale só para este mailer. |
'mailers' => [ 'coffeemail' => [ 'transport' => 'coffeemail', ], 'coffeemail-marketing' => [ 'transport' => 'coffeemail', 'api_key' => env('COFFEEMAIL_MARKETING_API_KEY'), ], ],
Cabeçalhos de controle
O transport lê estes cabeçalhos da mensagem e os converte em campos do EmailPayload; eles não são repassados como cabeçalhos customizados:
| Cabeçalho na mensagem | Campo no payload | Efeito na requisição |
|---|---|---|
X-Idempotency-Key ou Idempotency-Key |
idempotencyKey |
Header HTTP x-idempotency-key |
X-CoffeeMail-Sandbox (true/false) |
isSandbox |
Header HTTP x-coffeemail-sandbox: true quando verdadeiro |
X-CoffeeMail-Scheduled-At |
scheduledAt |
Campo scheduledAt no corpo |
From, To, Cc, Bcc, Reply-To, Subject, Content-Type, MIME-Version, Date e Message-ID são tratados como campos estruturados. Qualquer outro cabeçalho vai em headers.
Mapeamento do e-mail
| Mensagem Laravel/Symfony | Payload CoffeeMail |
|---|---|
Primeiro From |
from ({ email, name? }); obrigatório, senão InvalidArgumentException |
To |
to (lista); obrigatório, senão InvalidArgumentException |
Cc / Bcc |
cc / bcc |
Primeiro Reply-To |
replyTo (só o endereço; o nome é descartado) |
| Corpo HTML / texto | html / text |
| Anexos | attachments[] com filename (fallback attachment.bin), content em base64 e contentType |
🛡️ Tratamento de erros
No envio via Mail
O transport converte o envelope { data, error } do SDK em exceções do Symfony Mailer, que o Laravel propaga:
| Situação | Exceção |
|---|---|
| API ou rede retornou erro | Symfony\Component\Mailer\Exception\TransportException com mensagem Falha ao despachar e-mail via CoffeeMail: {mensagem} (Status: {status}), getCode() = status HTTP (0 em falha de rede) e getPrevious() = exceção tipada do SDK |
Mensagem sem From ou sem To |
InvalidArgumentException |
Mensagem que não é Symfony\Component\Mime\Email |
InvalidArgumentException |
| Mailer resolvido sem API key | CoffeeMail\Exceptions\AuthenticationError (lançada pelo construtor do SDK) |
use CoffeeMail\Exceptions\RateLimitError; use Symfony\Component\Mailer\Exception\TransportException; try { Mail::to($user)->send(new PedidoConfirmadoMail($pedido)); } catch (TransportException $e) { $sdkError = $e->getPrevious(); if ($sdkError instanceof RateLimitError) { $retryAfter = $sdkError->retryAfterSeconds; } }
O SDK não faz retry automático. Em Mailables ShouldQueue, a exceção falha o job e o retry fica com a configuração da fila do Laravel (tries, backoff).
Na facade
Os métodos dos recursos retornam CoffeeMail\Http\CoffeeMailResponse e não lançam exceção para erros HTTP. A resposta tem data, error (?CoffeeMailException), statusCode, isSuccess(), isError(), unwrap() (devolve data ou lança o erro) e toTuple(); também aceita desestruturação [$data, $error] = $response.
Classes em CoffeeMail\Exceptions, todas estendendo CoffeeMailException (status, errorCode, details):
| Classe | Status | Quando |
|---|---|---|
ValidationError |
400 | Payload inválido |
AuthenticationError |
401 | API key ausente ou inválida |
PaymentRequiredError |
402 | Plano/cota exige pagamento |
ForbiddenError |
403 | Chave sem permissão para o recurso |
NotFoundError |
404 | Recurso inexistente |
ConflictError |
409 | Conflito de estado ou duplicidade |
RateLimitError |
429 | Limite de taxa; expõe retryAfterSeconds (de Retry-After) |
InternalServerError |
demais | Qualquer outro status de erro (inclusive 422 e 5xx) |
NetworkException |
0 | Falha de cURL ou timeout |
📚 Referência
Acesso ao client
A facade encaminha chamadas de método ao singleton CoffeeMail\CoffeeMail. Os recursos são propriedades do client, não métodos nem membros estáticos: CoffeeMail::templates->list() não funciona (Undefined constant). Use a raiz da facade ou o container:
use CoffeeMail\CoffeeMail as CoffeeMailClient; use CoffeeMail\Laravel\Facades\CoffeeMail; $client = CoffeeMail::getFacadeRoot(); $client = app(CoffeeMailClient::class); $client = app('coffeemail'); [$templates, $error] = $client->templates->list();
Métodos chamáveis direto pela facade:
| Método | Retorno | Endpoint |
|---|---|---|
CoffeeMail::introspect(bool $forceRefresh = false) |
CoffeeMailResponse (cache local de 5 min) |
GET /v1/product/auth/me/api-key |
CoffeeMail::invalidateApiKeyCache() |
void |
— |
CoffeeMail::getApiKey() |
string |
— |
CoffeeMail::getLocale() |
string |
— |
CoffeeMail::getTransport() |
HttpTransportInterface |
— |
As tabelas abaixo refletem o SDK PHP 0.2.0, exigido por este pacote. Prefixo comum dos endpoints: /v1/product.
Mailables
use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Attachment; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Illuminate\Mail\Mailables\Headers; use Illuminate\Queue\SerializesModels; final class PedidoConfirmadoMail extends Mailable implements ShouldQueue { use Queueable, SerializesModels; public function __construct(public array $pedido) { } public function envelope(): Envelope { return new Envelope(subject: "Pedido #{$this->pedido['id']} confirmado"); } public function content(): Content { return new Content(view: 'emails.pedido-confirmado', with: ['pedido' => $this->pedido]); } public function headers(): Headers { return new Headers(text: [ 'X-Idempotency-Key' => "order_confirm_{$this->pedido['id']}", ]); } public function attachments(): array { return [ Attachment::fromPath(storage_path('recibos/recibo.pdf')) ->as('recibo.pdf') ->withMime('application/pdf'), ]; } } Mail::to($user->email)->send(new PedidoConfirmadoMail($pedido));
Com ShouldQueue, send() enfileira o Mailable na fila padrão do Laravel; o transport roda no worker. Notificações pelo canal mail usam o mailer default, logo passam pelo mesmo transport.
emails
| Método | Endpoint |
|---|---|
send(array|EmailPayload $payload) |
POST /emails |
sendBatch(list<array|EmailPayload> $items) |
POST /emails/batch |
get(string $id) |
GET /emails/{id} |
list(array $query = []) |
GET /emails |
getEvents(string $id) |
GET /emails/{id}/events |
getTags() |
GET /emails/tags |
cancel(string $id) |
POST /emails/{id}/cancel |
resend(string $id) |
POST /emails/{id}/resend |
[$email, $error] = CoffeeMail::getFacadeRoot()->emails->get('eml_123');
domains
| Método | Endpoint |
|---|---|
create(array|DomainPayload $payload) |
POST /domains |
list(array $query = []) |
GET /domains |
get(string $id) |
GET /domains/{id} |
verify(string $id) |
POST /domains/{id}/verify |
delete(string $id) |
DELETE /domains/{id} |
getHealth(string $id) |
POST /domains/{id}/health |
getWarmupStatus(string $id) |
GET /domains/{id}/warmup |
[$domain, $error] = CoffeeMail::getFacadeRoot()->domains->verify('dom_123');
templates
| Método | Endpoint |
|---|---|
create(array $payload) |
POST /templates |
list(array $query = []) |
GET /templates |
get(string $id) |
GET /templates/{id} |
update(string $id, array $payload) |
PATCH /templates/{id} |
delete(string $id) |
DELETE /templates/{id} |
preview(array $payload) |
POST /templates/preview |
previewById(string $id, array $variables = []) |
GET /templates/{id} + POST /templates/preview |
format(array $payload) |
POST /templates/format |
testRender(array $payload) |
POST /templates/test-render |
testSend(string $id, array $payload) |
POST /templates/{id}/test-send |
[$templates, $error] = CoffeeMail::getFacadeRoot()->templates->list();
audiences
| Método | Endpoint |
|---|---|
create(array $payload) |
POST /audiences |
list(array $query = []) |
GET /audiences |
get(string $id) |
GET /audiences/{id} |
update(string $id, array $payload) |
PUT /audiences/{id} |
delete(string $id) |
DELETE /audiences/{id} |
contacts->create(string $audienceId, array $payload) |
POST /audiences/{id}/contacts |
contacts->list(string $audienceId, array $query = []) |
GET /audiences/{id}/contacts |
contacts->bulkAdd(string $audienceId, array $contacts) |
POST /audiences/{id}/contacts/bulk |
contacts->update(string $audienceId, string $contactId, array $payload) |
PUT /audiences/{id}/contacts/{contactId} |
contacts->delete(string $audienceId, string $contactId) |
DELETE /audiences/{id}/contacts/{contactId} |
listContacts(), createContact() e bulkAddContacts() continuam como atalhos para contacts.
[$contacts, $error] = CoffeeMail::getFacadeRoot()->audiences->contacts->list('aud_123');
broadcasts
| Método | Endpoint |
|---|---|
create(array $payload) |
POST /broadcasts |
list(array $query = []) |
GET /broadcasts |
get(string $id) |
GET /broadcasts/{id} |
send(string $id) |
POST /broadcasts/{id}/send |
cancel(string $id) |
POST /broadcasts/{id}/cancel |
testSend(string $id, array $payload) |
POST /broadcasts/{id}/test-send |
Todas exigem chave full_access; sem ela, o erro é PermissionError. A API não tem exclusão de campanha: use cancel().
[$broadcast, $error] = CoffeeMail::getFacadeRoot()->broadcasts->send('brd_123');
suppressions
| Método | Endpoint |
|---|---|
list(array $query = []) |
GET /suppressions |
get(string $id) |
GET /suppressions/{id} |
create(array $payload) |
POST /suppressions |
delete(string $id) |
DELETE /suppressions/{id} |
reactivate(string $id) |
POST /suppressions/{id}/reactivate |
bulkCreate(array $suppressions) |
POST /suppressions/bulk |
[$suppressions, $error] = CoffeeMail::getFacadeRoot()->suppressions->list();
webhooks
| Método | Endpoint |
|---|---|
create(array|WebhookPayload $payload) |
POST /webhooks |
list(array $query = []) |
GET /webhooks |
get(string $id) |
GET /webhooks/{id} |
update(string $id, array $payload) |
PUT /webhooks/{id} |
delete(string $id) |
DELETE /webhooks/{id} |
toggle(string $id, bool $enabled) |
PATCH /webhooks/{id} |
rotateSecret(string $id) |
POST /webhooks/{id}/rotate-secret |
test(string $id) |
POST /webhooks/{id}/test |
listDeliveries(string $id, array $query = []) |
GET /webhooks/{id}/deliveries |
Webhooks::verifySignature(string $payload, string $secret, array $headers = [], ...) (estático) |
local; usado pelo middleware |
[$deliveries, $error] = CoffeeMail::getFacadeRoot()->webhooks->listDeliveries('wh_123');
stats
| Método | Endpoint |
|---|---|
get(array $query = []) |
GET /stats |
[$stats, $error] = CoffeeMail::getFacadeRoot()->stats->get(['from' => '2026-01-01', 'to' => '2026-01-31']);
senders
| Método | Endpoint |
|---|---|
create(string $email, string $displayName) |
POST /senders |
list() |
GET /senders |
verify(string $token) |
POST /senders/verify |
delete(string $id) |
DELETE /senders/{id} |
Detalhes dos payloads de cada recurso: README do SDK PHP.
🔔 Webhooks
O que o pacote oferece
O ServiceProvider registra o alias de middleware coffeemail.webhook (VerifyWebhookSignature). Ele:
- Lê
coffeemail.webhook_secret; se vazio, responde500. - Lê a assinatura de
x-webhook-signature; se ausente,401. - Valida com
CoffeeMail\Resources\Webhooks::verifySignature()do SDK PHP:sha256=+ HMAC-SHA256 de"{x-webhook-timestamp}.{x-webhook-idempotency-key}.{corpo bruto}", comparado em tempo constante, com o timestamp dentro decoffeemail.webhook_tolerance(padrão 300 s); se inválida,401. - Despacha
CoffeeMail\Laravel\Events\WebhookReceivedcompayload(corpo JSON decodificado),signature,idempotencyKeyetest(trueem disparos de teste), e segue para a rota.
use Illuminate\Support\Facades\Route; Route::post('/webhooks/coffeemail', fn () => response()->json(['received' => true])) ->middleware('coffeemail.webhook');
use CoffeeMail\Laravel\Events\WebhookReceived; final class ProcessarWebhookCoffeeMail { public function handle(WebhookReceived $event): void { $tipo = $event->payload['event_type'] ?? null; $dados = $event->payload['data'] ?? []; } }
Registre a rota fora do grupo web (ou exclua-a da verificação CSRF), já que o POST vem de servidor.
Cabeçalhos enviados pela API
| Cabeçalho | Conteúdo |
|---|---|
x-webhook-signature |
sha256= + HMAC-SHA256 hex de "{timestamp}.{idempotencyKey}.{corpo}" |
x-webhook-timestamp |
Unix epoch em segundos |
x-webhook-idempotency-key |
16 caracteres hex; muda a cada tentativa de entrega, então não serve para deduplicar retentativas |
x-webhook-test |
true em entregas de teste (POST /webhooks/{id}/test) |
O corpo traz event_type, webhook_id e data; data varia por evento e não inclui o id do email.
Eventos emitidos pela API (event_type): email.queued, email.scheduled, email.processing, email.sent, email.delivered, email.delivery_delayed, email.bounced, email.failed, email.cancelled, email.suppressed, email.complained, email.opened, email.clicked, email.unsubscribed.
🔖 Versionamento e releases
- SemVer. Versão publicada: v0.1.0; a
mainestá em 0.2.0 (não publicada). A versão vem da tag Git;composer.jsonnão tem campoversion. - Compatibilidade retroativa: a
0.2.0foi a exceção (corrigiu o middleware enquanto ainda não havia usuários ativos). A partir dela, a superfície pública (nome do transportcoffeemail, aliascoffeemail.webhook, chaves de config, classesCoffeeMailServiceProvider,CoffeeMailTransport,VerifyWebhookSignature,WebhookReceived, facadeCoffeeMail) só recebe adições e depreciações; nada é renomeado nem removido. - Publicação: push de uma tag
v*dispara.github/workflows/release.yml, que valida ocomposer.json, instala dependências (SDK PHP via repositório VCS), rodacomposer checke cria o GitHub Release com notas geradas automaticamente. O Packagist lê a tag do repositório. - Mudanças em CHANGELOG.md, no formato Keep a Changelog.
🧪 Testes e qualidade
Scripts (composer.json)
| Comando | Executa |
|---|---|
composer test |
pest (suítes Feature e Unit, via Orchestra Testbench) |
composer analyse |
phpstan analyse --memory-limit=512M, nível 9, em src e config |
composer lint |
phpcs com PSR-12 em src, config e tests |
composer format |
phpcbf (corrige o que o PSR-12 permitir) |
composer check:standards |
scripts/check-standards.php: falha se houver else, elseif/else if ou switch em src |
composer check |
lint → analyse → test → check:standards |
CI
.github/workflows/ci.yml roda em push e PR para main, com matriz PHP 8.2 / 8.3 / 8.4: composer validate, adiciona coffee-mail-sdk-php como repositório VCS, composer update, depois lint, analyse, test e check:standards.
O que os testes cobrem
tests/Unit/CoffeeMailTransportTest.php: conversãoEmail→ payload, múltiplos destinatários, cc/bcc/reply-to, anexos, cabeçalhos de controle eTransportExceptionem erro da API.tests/Feature/MailableDispatchTest.php: Mailable ponta a ponta peloMail.tests/Feature/ServiceProviderTest.php: singleton, alias, facade e resolução do mailer.tests/Feature/VerifyWebhookSignatureTest.php: assinatura válida, inválida e ausente (no esquema próprio do middleware, não no da API).
Os testes usam tests/Support/FakeHttpTransport.php, um HttpTransportInterface em memória. Ele é de uso interno, não faz parte do autoload de produção.
Testando a sua aplicação
use App\Mail\PedidoConfirmadoMail; use Illuminate\Support\Facades\Mail; test('checkout envia confirmação', function (): void { Mail::fake(); $this->post('/checkout', ['pedido' => 456]); Mail::assertSent(PedidoConfirmadoMail::class, fn ($mail) => $mail->hasTo('cliente@gmail.com')); });
🗂️ Estrutura do repositório
coffee-mail-laravel/
├── config/coffeemail.php # api_key, locale, webhook_secret
├── src/
│ ├── CoffeeMailServiceProvider.php # singleton do client, driver 'coffeemail', middleware, publish
│ ├── Facades/CoffeeMail.php
│ ├── Transport/CoffeeMailTransport.php # Symfony AbstractTransport
│ ├── Http/Middleware/VerifyWebhookSignature.php
│ └── Events/WebhookReceived.php
├── tests/
│ ├── Feature/ # provider, Mailable, middleware
│ ├── Unit/ # transport
│ └── Support/ # FakeHttpTransport, Mailable de teste
├── scripts/check-standards.php
├── .github/workflows/{ci,release}.yml
├── phpstan.neon · phpcs.xml · phpunit.xml.dist
└── composer.json
🤝 Contribuição
- Rode
composer checkantes de abrir PR; é o mesmo gate da release. - Código em inglês; mensagens de commit e PR em pt-BR.
- Sem
else/elseif/switchemsrc(early return oumatch), PSR-12, PHPStan nível 9 limpo. - Toda mudança de superfície pública é aditiva (ver Versionamento).
- Localmente,
vendor/coffeemail/coffeemail-phppode ser um symlink para o checkout do SDK; o CI resolve a versão publicada. Teste contra ela antes de depender de algo que só existe namaindo SDK.
🔗 Projetos relacionados
| Repositório | Papel |
|---|---|
| coffee-mail-sdk-node | SDK oficial Node.js/TypeScript (@coffeemail/node) |
| coffee-mail-sdk-python | SDK oficial Python (coffeemail) |
| coffee-mail-sdk-php | SDK oficial PHP (coffeemail/coffeemail-php); base deste pacote |
| coffee-mail-sdk-java | SDK oficial Java (br.com.coffeemail:coffeemail-java) |
| coffee-mail-sdk-go | SDK oficial Go (github.com/Coffee-Mail/coffee-mail-sdk-go) |
| coffee-mail-sdk-ruby | SDK oficial Ruby (coffeemail) |
| coffee-mail-laravel (você está aqui) | Integração Laravel (coffeemail/coffeemail-laravel): mail transport + facade |
| coffee-mail-sdk-workbench | Bancada de validação dos SDKs por spec |
📄 Licença
CoffeeMail SDK License, licença proprietária (LICENSE; composer.json declara "license": "proprietary"). Diferente dos SDKs Node, Python, PHP, Java, Go e Ruby, que são MIT. Em resumo:
- Copyright da CoffeeMail, todos os direitos reservados. O software é licenciado, não vendido.
- Uso permitido somente mediante acordo de licença comercial escrito e assinado por representante autorizado da CoffeeMail. A cópia em registros públicos (Packagist) serve apenas para integração por clientes autorizados da plataforma.
- Sem autorização prévia e por escrito, é proibido copiar, modificar, mesclar, publicar, distribuir, sublicenciar ou vender; fazer engenharia reversa, descompilar ou desmontar (salvo onde a lei permitir); e remover avisos de propriedade.
- Fornecido "no estado em que se encontra", sem garantias.
- Licenciamento comercial:
licensing@coffeemail.com.
O texto do LICENSE prevalece sobre este resumo. A tag publicada v0.1.0 não contém o arquivo LICENSE; ele foi adicionado depois, na main.