diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..08b448f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/vendor/
+/node_modules/
+.phpunit.cache/
+.phpunit.result.cache
+composer.lock
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ff1c817
--- /dev/null
+++ b/README.md
@@ -0,0 +1,106 @@
+# LeadMail SDK for Laravel
+
+Laravel package for sending emails and verifying email addresses through the [leadMail](https://mail.leadmagnet.dev) service.
+
+## Requirements
+
+- PHP 8.2+
+- Laravel 11 or 12
+
+## Installation
+
+```bash
+composer require leadm/leadmail
+```
+
+Publish the config file:
+
+```bash
+php artisan vendor:publish --provider="LeadM\LeadMail\LeadMailServiceProvider"
+```
+
+## Configuration
+
+Add these to your `.env`:
+
+```env
+LEADMAIL_URL=https://mail.leadmagnet.dev
+LEADMAIL_TOKEN=lm_your_api_token_here
+```
+
+Optional settings:
+
+```env
+LEADMAIL_TIMEOUT=30
+LEADMAIL_VERIFY_SSL=true
+LEADMAIL_AUTO_TENANT=true
+```
+
+## Usage
+
+### Send Emails via Mail Driver
+
+Set `leadmail` as your mail driver in `.env`:
+
+```env
+MAIL_MAILER=leadmail
+```
+
+Then use Laravel's `Mail` facade as usual:
+
+```php
+Mail::to('user@example.com')->send(new WelcomeMail());
+```
+
+### Send Emails via API
+
+```php
+LeadMail::sendEmail([
+ 'from' => ['email' => 'hello@yourdomain.com', 'name' => 'Your App'],
+ 'to' => [['email' => 'user@example.com', 'name' => 'User']],
+ 'subject' => 'Welcome!',
+ 'html_body' => '
Welcome to our app
',
+]);
+```
+
+### Verify Email Addresses
+
+```php
+$result = LeadMail::verifyEmail('user@example.com');
+
+if ($result['data']['valid']) {
+ // Email is deliverable
+}
+```
+
+### Validation Rule
+
+Use the `leadmail_verify` rule in your form requests:
+
+```php
+public function rules(): array
+{
+ return [
+ 'email' => ['required', 'email', 'leadmail_verify'],
+ ];
+}
+```
+
+### Get Allowed Sender Domains
+
+```php
+$domains = LeadMail::getDomains();
+// ['yourdomain.com', 'anotherdomain.com']
+```
+
+## Multi-Tenancy
+
+If your app uses [stancl/tenancy](https://tenancyforlaravel.com), the SDK automatically includes the current tenant ID in API requests via the `X-Tenant-Id` header. Disable this with:
+
+```env
+LEADMAIL_AUTO_TENANT=false
+```
+
+## License
+
+MIT
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..37d9cd1
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,49 @@
+{
+ "name": "leadm/leadmail",
+ "description": "Laravel SDK for leadMail email sending and verification service",
+ "type": "library",
+ "license": "MIT",
+ "homepage": "https://github.com/leadmnik/leadmail-sdk",
+ "authors": [
+ {
+ "name": "Lead Magnet Oy",
+ "homepage": "https://leadmagnet.fi"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/leadmnik/leadmail-sdk/issues",
+ "source": "https://github.com/leadmnik/leadmail-sdk"
+ },
+ "require": {
+ "php": "^8.2",
+ "guzzlehttp/guzzle": "^7.0",
+ "illuminate/mail": "^11.0|^12.0",
+ "illuminate/support": "^11.0|^12.0"
+ },
+ "require-dev": {
+ "orchestra/testbench": "^9.0|^10.0",
+ "pestphp/pest": "^3.0|^4.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "LeadM\\LeadMail\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "LeadM\\LeadMail\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "LeadM\\LeadMail\\LeadMailServiceProvider"
+ ],
+ "aliases": {
+ "LeadMail": "LeadM\\LeadMail\\LeadMailFacade"
+ }
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/config/leadmail.php b/config/leadmail.php
new file mode 100644
index 0000000..44368b8
--- /dev/null
+++ b/config/leadmail.php
@@ -0,0 +1,55 @@
+ env('LEADMAIL_URL', 'https://mail.leadmagnet.dev'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | API Token
+ |--------------------------------------------------------------------------
+ |
+ | The bearer token for authenticating with the leadMail API.
+ | Generate this in the leadMail admin dashboard.
+ |
+ */
+ 'token' => env('LEADMAIL_TOKEN'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Request Timeout
+ |--------------------------------------------------------------------------
+ |
+ | Maximum number of seconds to wait for API responses.
+ |
+ */
+ 'timeout' => env('LEADMAIL_TIMEOUT', 30),
+
+ /*
+ |--------------------------------------------------------------------------
+ | SSL Verification
+ |--------------------------------------------------------------------------
+ |
+ | Whether to verify SSL certificates. Disable only for local development.
+ |
+ */
+ 'verify_ssl' => env('LEADMAIL_VERIFY_SSL', true),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Auto Tenant Detection
+ |--------------------------------------------------------------------------
+ |
+ | When enabled and stancl/tenancy is installed, automatically sends the
+ | current tenant's key as the X-Tenant-Id header on every request.
+ |
+ */
+ 'auto_tenant' => env('LEADMAIL_AUTO_TENANT', true),
+];
diff --git a/src/LeadMailClient.php b/src/LeadMailClient.php
new file mode 100644
index 0000000..ae0e9ad
--- /dev/null
+++ b/src/LeadMailClient.php
@@ -0,0 +1,133 @@
+http = new Client([
+ 'base_uri' => rtrim($this->baseUrl, '/').'/api/v1/',
+ 'timeout' => $this->timeout,
+ 'verify' => $this->verifySsl,
+ ]);
+ }
+
+ /**
+ * Send an email through the leadMail service.
+ *
+ * @param array{from: array, to: array, cc?: array, bcc?: array, reply_to?: array, subject: string, html_body?: string, text_body?: string, attachments?: array, metadata?: array, options?: array} $data
+ * @return array{success: bool, data: array{log_id: int, status: string}}
+ *
+ * @throws GuzzleException
+ */
+ public function sendEmail(array $data): array
+ {
+ $response = $this->http->post('emails/send', [
+ 'headers' => $this->headers(),
+ 'json' => $data,
+ ]);
+
+ return json_decode($response->getBody()->getContents(), true);
+ }
+
+ /**
+ * Get the allowed sender domains for this client app.
+ *
+ * @return string[]
+ *
+ * @throws GuzzleException
+ */
+ public function getDomains(): array
+ {
+ $response = $this->http->get('domains', [
+ 'headers' => $this->headers(),
+ ]);
+
+ $data = json_decode($response->getBody()->getContents(), true);
+
+ return $data['data']['domains'] ?? [];
+ }
+
+ /**
+ * Verify an email address through the leadMail service.
+ *
+ * @return array{success: bool, data: array{email: string, valid: bool, status: string, reason: ?string, cached: bool}}
+ */
+ public function verifyEmail(string $email, bool $allowDisposable = false): array
+ {
+ try {
+ $response = $this->http->post('emails/verify', [
+ 'headers' => $this->headers(),
+ 'json' => [
+ 'email' => $email,
+ 'allow_disposable' => $allowDisposable,
+ ],
+ ]);
+
+ return json_decode($response->getBody()->getContents(), true);
+ } catch (GuzzleException $e) {
+ Log::warning('LeadMail verification failed, failing open', [
+ 'email' => $email,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return [
+ 'success' => true,
+ 'data' => [
+ 'email' => $email,
+ 'valid' => true,
+ 'status' => 'unknown',
+ 'reason' => null,
+ 'cached' => false,
+ ],
+ ];
+ }
+ }
+
+ /**
+ * @return array
+ */
+ protected function headers(): array
+ {
+ $headers = [
+ 'Authorization' => 'Bearer '.$this->token,
+ 'Accept' => 'application/json',
+ 'Content-Type' => 'application/json',
+ ];
+
+ if ($this->autoTenant && $this->hasTenancy()) {
+ $tenantId = $this->resolveTenantId();
+ if ($tenantId !== null) {
+ $headers['X-Tenant-Id'] = $tenantId;
+ }
+ }
+
+ return $headers;
+ }
+
+ protected function hasTenancy(): bool
+ {
+ return class_exists(\Stancl\Tenancy\Tenancy::class);
+ }
+
+ protected function resolveTenantId(): ?string
+ {
+ if (! function_exists('tenancy') || ! tenancy()->initialized) {
+ return null;
+ }
+
+ return (string) tenant()->getTenantKey();
+ }
+}
diff --git a/src/LeadMailFacade.php b/src/LeadMailFacade.php
new file mode 100644
index 0000000..96c5846
--- /dev/null
+++ b/src/LeadMailFacade.php
@@ -0,0 +1,20 @@
+mergeConfigFrom(__DIR__.'/../config/leadmail.php', 'leadmail');
+
+ $this->app->singleton(LeadMailClient::class, function () {
+ return new LeadMailClient(
+ baseUrl: config('leadmail.url'),
+ token: config('leadmail.token', ''),
+ timeout: config('leadmail.timeout', 30),
+ verifySsl: config('leadmail.verify_ssl', true),
+ autoTenant: config('leadmail.auto_tenant', true),
+ );
+ });
+ }
+
+ public function boot(): void
+ {
+ if ($this->app->runningInConsole()) {
+ $this->publishes([
+ __DIR__.'/../config/leadmail.php' => config_path('leadmail.php'),
+ ], 'leadmail-config');
+ }
+
+ Mail::extend('leadmail', function () {
+ return new LeadMailTransport(
+ $this->app->make(LeadMailClient::class),
+ );
+ });
+
+ Validator::extend('leadmail_verify', function (string $attribute, mixed $value) {
+ $rule = $this->app->make(LeadMailVerify::class);
+ $passed = true;
+
+ $rule->validate($attribute, $value, function () use (&$passed) {
+ $passed = false;
+ });
+
+ return $passed;
+ }, 'The :attribute email address could not be verified.');
+ }
+}
diff --git a/src/LeadMailTransport.php b/src/LeadMailTransport.php
new file mode 100644
index 0000000..4574897
--- /dev/null
+++ b/src/LeadMailTransport.php
@@ -0,0 +1,105 @@
+getOriginalMessage());
+
+ $payload = $this->buildPayload($email);
+
+ $result = $this->client->sendEmail($payload);
+
+ if (isset($result['data']['log_id'])) {
+ $message->getOriginalMessage()->getHeaders()->addTextHeader(
+ 'X-LeadMail-Log-Id',
+ (string) $result['data']['log_id'],
+ );
+ }
+ }
+
+ /**
+ * @return array
+ */
+ protected function buildPayload(Email $email): array
+ {
+ $payload = [
+ 'from' => $this->formatAddress($email->getFrom()[0] ?? null),
+ 'to' => array_map(fn (Address $a) => $this->formatAddress($a), $email->getTo()),
+ 'subject' => $email->getSubject() ?? '',
+ ];
+
+ if ($email->getCc()) {
+ $payload['cc'] = array_map(fn (Address $a) => $this->formatAddress($a), $email->getCc());
+ }
+
+ if ($email->getBcc()) {
+ $payload['bcc'] = array_map(fn (Address $a) => $this->formatAddress($a), $email->getBcc());
+ }
+
+ if ($email->getReplyTo()) {
+ $replyTo = $email->getReplyTo()[0];
+ $payload['reply_to'] = $this->formatAddress($replyTo);
+ }
+
+ if ($email->getHtmlBody()) {
+ $payload['html_body'] = $email->getHtmlBody();
+ }
+
+ if ($email->getTextBody()) {
+ $payload['text_body'] = $email->getTextBody();
+ }
+
+ $attachments = [];
+ foreach ($email->getAttachments() as $attachment) {
+ $attachments[] = [
+ 'content' => base64_encode($attachment->getBody()),
+ 'filename' => $attachment->getFilename() ?? 'attachment',
+ 'mime_type' => $attachment->getMediaType().'/'.$attachment->getMediaSubtype(),
+ ];
+ }
+
+ if ($attachments) {
+ $payload['attachments'] = $attachments;
+ }
+
+ return $payload;
+ }
+
+ /**
+ * @return array{email: string, name?: string}
+ */
+ protected function formatAddress(?Address $address): array
+ {
+ if ($address === null) {
+ return ['email' => ''];
+ }
+
+ $result = ['email' => $address->getAddress()];
+
+ if ($address->getName()) {
+ $result['name'] = $address->getName();
+ }
+
+ return $result;
+ }
+
+ public function __toString(): string
+ {
+ return 'leadmail';
+ }
+}
diff --git a/src/Rules/LeadMailVerify.php b/src/Rules/LeadMailVerify.php
new file mode 100644
index 0000000..6e38a72
--- /dev/null
+++ b/src/Rules/LeadMailVerify.php
@@ -0,0 +1,35 @@
+client->verifyEmail($value, $this->allowDisposable);
+
+ if (! ($result['data']['valid'] ?? true)) {
+ $reason = $result['data']['reason'] ?? 'verification failed';
+ $fail("The :attribute email address is not deliverable ({$reason}).");
+ }
+ } catch (\Throwable) {
+ // Fail open — if the verification service is down, allow the email through
+ }
+ }
+}