offline-agency/laravel-email-chef

This is a simple Laravel package for integration with Email Chef API

Maintainers

Package info

github.com/offline-agency/laravel-email-chef

Homepage

pkg:composer/offline-agency/laravel-email-chef

Fund package maintenance!

OfflineAgency

Statistics

Installs: 135

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.0.0 2026-04-07 11:05 UTC

This package is auto-updated.

Last update: 2026-04-07 11:06:16 UTC


README

Latest Stable Version PHP Version Tests codecov PHPStan Total Downloads License

A Laravel package for the EmailChef API — covering all 14 resource groups with a fluent, typed PHP interface.

Requirements

Dependency Version
PHP ^8.4
Laravel ^12.0 | ^13.0
orchestra/testbench (dev) ^10.0 | ^11.0

Installation

composer require offline-agency/laravel-email-chef

Publish the config file:

php artisan vendor:publish --provider="OfflineAgency\LaravelEmailChef\LaravelEmailChefServiceProvider" --tag="laravel-email-chef-config"

Add your credentials to .env:

EMAIL_CHEF_USERNAME=your@email.com
EMAIL_CHEF_PASSWORD=your-password

The published config (config/email-chef.php):

return [
    'baseUrl'    => 'https://app.emailchef.com/apps/api/v1/',
    'login_url'  => 'https://app.emailchef.com/api/',
    'username'   => env('EMAIL_CHEF_USERNAME'),
    'password'   => env('EMAIL_CHEF_PASSWORD'),
    'list_id'    => '97322',
    'contact_id' => '656023',
];

Usage

Every API class is instantiated directly — authentication is handled automatically via JWT.

Account

use OfflineAgency\LaravelEmailChef\Api\Resources\AccountApi;

$account = (new AccountApi())->getCollection();
// Returns AccountEntity with id, email, lang, status, subscribers, etc.
echo $account->email; // "admin@acme.com"

Account Infos

use OfflineAgency\LaravelEmailChef\Api\Resources\AccountInfosApi;

$api    = new AccountInfosApi();
$info   = $api->getInstance('12345');
$result = $api->update([
    'company_name' => 'Acme Corp',
    'address'      => 'Via Roma 1, Milan',
]);

Subscription

use OfflineAgency\LaravelEmailChef\Api\Resources\SubscriptionApi;

$subscription = (new SubscriptionApi())->getCollection();
echo $subscription->type;            // "premium"
echo $subscription->plan_expiration; // "2027-01-15"

Lists

use OfflineAgency\LaravelEmailChef\Api\Resources\ListsApi;

$lists = new ListsApi();

// Browse all lists
$all = $lists->getCollection(limit: 10, offset: 0, orderby: 'name', order_type: 'asc');

// Get details and stats
$list  = $lists->getInstance('97322');
$stats = $lists->getStats('97322', '2024-01-01', '2024-12-31');

// Create, update, delete
$created = $lists->create(['list_name' => 'Newsletter', 'list_description' => 'Main list']);
$lists->update('97322', ['list_name' => 'Updated Newsletter', 'list_description' => 'Updated']);
$lists->delete('97322');

// Subscribe / Unsubscribe
$lists->subscribe('97322', '656023');
$lists->unsubscribe('97322', '656023');

Contacts

use OfflineAgency\LaravelEmailChef\Api\Resources\ContactsApi;

$contacts = new ContactsApi();

$count   = $contacts->count('97322');
$all     = $contacts->getCollection('active', '97322', limit: 25, offset: 0, order_by: 'email', order_type: 'asc');
$contact = $contacts->getInstance('656023', '97322');

$created = $contacts->create([
    'list_id' => '97322',
    'email'   => 'john@example.com',
    'firstname' => 'John',
    'lastname'  => 'Doe',
]);

$contacts->update('656023', ['firstname' => 'Jane']);
$contacts->delete('97322', '656023');

Predefined Fields

use OfflineAgency\LaravelEmailChef\Api\Resources\PredefinedFieldsApi;

$fields = (new PredefinedFieldsApi())->getCollection();
// Collection of PredefinedFieldsEntity with id, name, type_id, reference, etc.

Custom Fields

use OfflineAgency\LaravelEmailChef\Api\Resources\CustomFieldsApi;

$api = new CustomFieldsApi();

$fields = $api->getCollection('97322');
$field  = $api->getInstance('42');
$count  = $api->count('97322');

$api->create('97322', ['name' => 'Birthday', 'type_id' => '3']);
$api->update('42', ['name' => 'Birth Date']);
$api->delete('42');

Blockings

use OfflineAgency\LaravelEmailChef\Api\Resources\BlockingsApi;

$api = new BlockingsApi();

$blocked = $api->getCollection('spam', limit: 10, offset: 0);
$count   = $api->count('spam');

$api->create('block@example.com', 'email');
$api->delete('block@example.com');

Import Tasks

use OfflineAgency\LaravelEmailChef\Api\Resources\ImportTasksApi;

$api = new ImportTasksApi();

$tasks = $api->getCollection();
$task  = $api->getInstance('101');

$api->create('97322', [
    'contacts' => [
        ['email' => 'a@example.com', 'firstname' => 'Alice'],
        ['email' => 'b@example.com', 'firstname' => 'Bob'],
    ],
]);

Segments

use OfflineAgency\LaravelEmailChef\Api\Resources\SegmentsApi;

$api = new SegmentsApi();

$segments      = $api->getCollection('97322', limit: 10, offset: 0);
$segment       = $api->getInstance('5');
$segmentCount  = $api->getCount('97322');
$contactsCount = $api->getContactsCount('5');

$api->createInstance(97322, [
    'name'  => 'VIP Customers',
    'logic' => 'and',
    'condition_groups' => [['field' => 'email', 'operator' => 'contains', 'value' => '@acme.com']],
]);
$api->updateInstance('97322', '5', ['name' => 'Premium VIPs']);
$api->deleteInstance('5');

Campaigns

use OfflineAgency\LaravelEmailChef\Api\Resources\CampaignsApi;

$api = new CampaignsApi();

$count     = $api->getCount();
$campaigns = $api->getCollection('sent', limit: 10, offset: 0, orderby: 'name', ordertype: 'asc');
$campaign  = $api->getInstance('10');

$api->createInstance([
    'name'       => 'Summer Sale',
    'subject'    => 'Up to 50% off',
    'from_name'  => 'Acme Store',
    'from_email' => 'hello@acme.com',
    'html_body'  => '<h1>Summer Sale!</h1><p>Shop now.</p>',
]);
$api->updateInstance('10', ['subject' => 'Extended: Summer Sale']);
$api->deleteInstance('10');

$api->sendTestEmail('10', ['email' => 'test@acme.com']);
$api->sendCampaign('10', []);
$api->schedule('10', ['send_time' => '2026-07-01 09:00:00']);
$api->cancelScheduling('10');
$api->archive('10');
$api->unarchive('10');
$api->cloning(['id' => '10']);
$api->getLinkCollection('10');

Autoresponders

use OfflineAgency\LaravelEmailChef\Api\Resources\AutorespondersApi;

$api = new AutorespondersApi();

$count = $api->getCount();
$list  = $api->getCollection(limit: 10, offset: 0, orderby: 'name', ordertype: 'asc');
$ar    = $api->getInstance('20');

$api->createInstance([
    'name'      => 'Welcome Email',
    'subject'   => 'Welcome aboard!',
    'html_body' => '<p>Thanks for joining.</p>',
]);
$api->updateInstance('20', ['subject' => 'Welcome to Acme!']);
$api->deleteInstance('20');

$api->sendTestEmail('20', ['email' => 'test@acme.com']);
$api->activate('20', []);
$api->deactivate('20', []);
$api->cloning(['id' => '20']);
$api->getLinksCollection('20');

Send Mail (transactional)

use OfflineAgency\LaravelEmailChef\Api\Resources\SendEmailApi;

(new SendEmailApi())->sendMail([
    'to'      => 'customer@example.com',
    'subject' => 'Your order has shipped',
    'html'    => '<p>Track your order <a href="https://track.acme.com/123">here</a>.</p>',
]);

SMS

use OfflineAgency\LaravelEmailChef\Api\Resources\SMSApi;

$sms = new SMSApi();

$sms->send(['to' => '+39 333 1234567', 'text' => 'Your verification code is 4821.']);
$sms->getBalance();                    // Balance entity with ->balance, ->currency
$sms->getStatusMessage('msg-abc123');  // StatusMessage entity
$sms->getBulkMessageStatus('bulk-1');  // BulkMessageStatus entity

API Coverage

Group Status
Account
Account Infos
Subscription
Lists
Contacts
Predefined Fields
Custom Fields
Blockings
Import Tasks
Segments
Campaigns
Autoresponders
Send Mail
SMS

Testing

composer test                            # run all tests
./vendor/bin/pest --coverage             # with coverage report
./vendor/bin/pest --coverage --min=80    # enforce coverage gate
composer analyse                         # static analysis (PHPStan level 6)
composer lint                            # fix code style
composer lint:test                       # check code style (dry-run)

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please report security issues to support@offlineagency.com.

Credits

License

The MIT License (MIT). Please see License File for more information.

Proposed Improvements

Note: This section is a review aid for PR #11. It will be removed before merging.

Proposal 1 — JWT token caching

Current: A new JWT token is fetched via POST /login on every API class instantiation.

Proposed: Cache the token using Laravel's Cache facade with a TTL slightly shorter than server-side expiry (e.g. 55 minutes):

use Illuminate\Support\Facades\Cache;

private function getToken(): string
{
    return Cache::remember('emailchef_jwt', now()->addMinutes(55), function (): string {
        $response = Http::post(config('email-chef.login_url').'login', [
            'username' => config('email-chef.username'),
            'password' => config('email-chef.password'),
        ]);
        return $response->json('authkey');
    });
}

Effort: Low. Impact: Eliminates redundant auth round-trips.

Proposal 2 — Exception hierarchy

Current: API errors return an Error entity. Consumers cannot catch specific error types.

Proposed: Create src/Exceptions/ hierarchy:

EmailChefException (base)
├── AuthenticationException   (401)
├── NotFoundException          (404)
├── ValidationException        (422 — carries field errors)
├── RateLimitException         (429 — includes Retry-After value)
└── ServerException            (5xx)

Effort: Medium. Impact: High — enables typed error handling.

Proposal 3 — EmailChef Facade

Current: Users instantiate each API class manually (new ListsApi()).

Proposed: A single-entry-point facade:

use OfflineAgency\LaravelEmailChef\Facades\EmailChef;

EmailChef::lists()->getCollection(limit: 10, offset: 0, orderby: 'name', order_type: 'asc');
EmailChef::contacts()->count(listId: '97322');
EmailChef::campaigns()->sendCampaign('42', []);

Effort: Low-medium. Impact: High ergonomic value.

Proposal 4 — Pagination abstraction

Current: List endpoints return a single page. No standard way to iterate beyond page 1.

Proposed: A PaginatedResponse value object with hasMorePages() and an ->all() convenience method that auto-fetches all pages.

Effort: Medium. Impact: Important for large contact lists.

Proposal 5 — Config type-safety

Current: Config keys like 'email-chef.baseUrl' are raw strings. A typo silently returns null.

Proposed: A src/EmailChefConfig.php class with static accessors that throw on missing config:

final class EmailChefConfig
{
    public static function baseUrl(): string
    {
        return config('email-chef.baseUrl') ?? throw new \RuntimeException('EmailChef baseUrl not configured.');
    }
}

Effort: Low. Impact: IDE autocompletion + early failure on misconfiguration.

Proposal 6 — Laravel 13 attribute adoption in examples

Laravel 13 introduced first-party PHP Attribute support. The README examples could demonstrate modern L13 usage patterns:

use Illuminate\Routing\Attributes\Controllers\Middleware;

#[Middleware('auth')]
class NewsletterController
{
    public function subscribe(Request $request): JsonResponse
    {
        (new ListsApi())->subscribe(
            listId: config('email-chef.list_id'),
            data: $request->validated(),
        );
        return response()->json(['subscribed' => true]);
    }
}

Effort: Documentation only.