Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Freshmailbundle Laravel Package

qbitz/freshmailbundle

FreshMailBundle provides a PHP/Symfony integration for the FreshMail email marketing API, making it easy to manage mailing lists, subscribers, campaigns, and transactional messages through a simple, configurable service wrapper.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony2 bundle, not a Laravel package. There is no service provider, no facade, no publishable config/freshmail.php, and no package discovery. Running composer require qbitz/freshmailbundle in a Laravel app gets you a class extending Symfony\Component\HttpKernel\Bundle\Bundle and a composer.json pinned to a dead framework line — not a working FreshMail client. Day-to-day, you do not install it. You read it as reference code and port the ~200 lines that matter.

Where to look first, in this order:

  1. The core service class — something like FreshmailService, FreshMail, or an Api class. This is the only part with real day-to-day value: it encodes the endpoint paths, payload shapes, and auth signing for subscriber/list/campaign operations.
  2. DependencyInjection/ and any example config — tells you the credential names (API key + API secret) and the base URL/API version the client was written against. In Laravel this becomes a config/services.php entry, not a YAML tree.
  3. composer.json — check the PHP constraint, Guzzle version, and symfony/symfony pin. Expect PHP >=5.3 and Guzzle ~3.0. That confirms the code is pre-PSR-4-strict, pre-types, and unusable verbatim.
  4. The HTTP call sites — every endpoint the bundle touches, and every place it builds a signature. That is the list of request contracts you are extracting.

First real use case — stand up a ported client behind an interface. Scope it deliberately; the bundle's domain surface is small.

// app/Contracts/FreshMailClient.php
interface FreshMailClient
{
    public function subscribe(string $listHash, Subscriber $subscriber): void;
    public function unsubscribe(string $listHash, string $email): void;
    public function sendCampaign(Campaign $campaign): string;
    public function sendTransactional(Message $message): void;
}
// config/services.php
'freshmail' => [
    'key'       => env('FRESHMAIL_API_KEY'),
    'secret'    => env('FRESHMAIL_API_SECRET'),
    'base_url'  => env('FRESHMAIL_BASE_URL', 'https://api.freshmail.com/rest/'),
    'timeout'   => (int) env('FRESHMAIL_TIMEOUT', 10),
    'retries'   => (int) env('FRESHMAIL_RETRIES', 3),
],

Suggested sequence (from a cold start, roughly 2–3 days):

  1. Vendor validation spike (1 day) — confirm FreshMail's API is live and versioned, capture current docs, and test auth against a sandbox account. If FreshMail is sunsetting, stop here. That's the cheapest possible exit and it invalidates everything below.
  2. Contract extraction (0.5 day) — diff the bundle's endpoints against current vendor docs; catalogue drift in URLs, field names, and error envelopes.
  3. Client implementation (1–2 days) — service provider, config, typed DTOs, Http transport with timeout/retry.
  4. Contract tests (1 day) — Http::fake() fixtures, never live calls in CI.
  5. Consume behind a feature flag, running in parallel with any existing manual process before cutover.

Do not force the bundle in via a compatibility shim. Importing an EOL framework's bootstrapping into a live Laravel app is negative value.

Implementation Patterns

Bind the interface, never the vendor class. The whole point of the port is that swapping to Brevo, Mailchimp, or Klaviyo later is a one-class change.

// app/Providers/FreshMailServiceProvider.php
public function register(): void
{
    $this->app->singleton(FreshMailClient::class, fn ($app) => new HttpFreshMailClient(
        key:     config('services.freshmail.key'),
        secret:  config('services.freshmail.secret'),
        baseUrl: config('services.freshmail.base_url'),
    ));
}

Mirror the bundle's auth signing, then verify it. The signature construction is the single most valuable thing you lift — and the single most likely thing to have drifted. Copy the recipe from the source, mark it clearly, and confirm against live docs before it ships.

final class HttpFreshMailClient implements FreshMailClient
{
    public function __construct(
        private readonly string $key,
        private readonly string $secret,
        private readonly string $baseUrl,
    ) {}

    private function client(): PendingRequest
    {
        return Http::baseUrl($this->baseUrl)
            ->acceptJson()
            ->timeout(config('services.freshmail.timeout'))
            ->retry(config('services.freshmail.retries'), 500, throw: false);
    }

    /** Signature recipe lifted from the bundle's API class — re-verify against current docs. */
    private function sign(string $action, array $payload): array
    {
        return [
            ...$payload,
            'apikey' => $this->key,
            'sign'   => sha1($this->key.'/'.$this->baseUrl.$action.'/'.$this->secret),
        ];
    }
}

Keep credentials out of the URL and out of logs. FreshMail historically accepted key/secret as request parameters. If the port does too, scrub them in logging middleware and never let them land in access logs or queued job payloads.

Normalise the response envelope once. The bundle returns raw decoded arrays and lets callers pick through status fields. Push that into a DTO boundary so every caller sees the same shape and 4xx bodies become exceptions, not false.

private function call(string $action, array $payload): array
{
    $response = $this->client()->post($action, $this->sign($action, $payload));

    throw_unless(
        $response->successful(),
        FreshMailException::fromResponse($action, $response),
    );

    return $response->json();
}

Model list sync as checkpointed batch jobs, not a loop. The bundle gives you no batching, no backoff, no resumability. A 10,000-subscriber sync that dies at 6,000 must resume from 6,000.

Bus::batch(
    $subscribers->chunk(500)->map(fn ($chunk) => new SyncSubscriberChunk($listHash, $chunk))
)->allowFailures()->name('freshmail-subscriber-sync')->dispatch();
final class SyncSubscriberChunk implements ShouldQueue
{
    use InteractsWithQueue, Queueable;

    public int $tries = 5;
    public array $backoff = [10, 60, 300];  // the bundle has no backoff; this is yours

    public function handle(FreshMailClient $client, RateLimiter $limiter): void
    {
        foreach ($this->subscribers as $subscriber) {
            $limiter->attempt('freshmail', 5, fn () => $client->subscribe($this->listHash, $subscriber), 1);
        }
    }
}

Queue every send. Transactional sends inside a request path convert a vendor latency spike into user-facing latency. sendTransactional() should only ever be called from a job, never a controller.

Contract-test with fixtures, not the network. Record real responses once during the spike, then assert the client sends what you expect.

Http::fake(['api.freshmail.com/rest/*' => Http::response(['status' => 'OK'], 200)]);

$client->subscribe('abc123', new Subscriber('[email protected]'));

Http::assertSent(fn (Request $req) => $req['apikey'] === 'test-key'
    && $req->url() === 'https://api.freshmail.com/rest/subscriber/add');

Optional: a facade for ergonomics. Only worth it once the interface is stable. FreshMail::subscribe(...) reads nicer in controllers than constructor injection, but the underlying binding must stay the interface.

Keep the code layer strictly transport-and-sync. Segmentation, consent state, template ownership, and suppression logic belong in marketing ops tooling or a dedicated domain service — not in your HTTP client. The bundle draws no such line; draw it yourself.

Gotchas and Tips

Composer resolution is a hard blocker. PHP 5.x constraints and a Guzzle 3/4 pin will not coexist with Laravel 10/11's PHP 8.1+ / Guzzle 7 expectations. A fifteen-minute check of composer.json confirms the extract-don't-depend estimate is right. Do not attempt --ignore-platform-reqs; you'll trade a resolution error for a runtime fatal.

Symfony2 concepts have no mechanical Laravel mapping. The Bundle class, DependencyInjection/*Extension, and Configuration tree are container bootstrapping, not configuration. There is no shim that makes this safe — you'd be loading an EOL kernel into a supported app.

The API assumptions are frozen at the last commit. Endpoint paths, field names, and error envelopes may have moved. Assume schema drift until you've diffed against current docs, and expect malformed payloads to be rejected at the vendor with unhelpful messages rather than caught locally. Validate payloads before sending.

Rate limits (429) are unhandled in the source. Without backoff, bulk operations cascade-fail. Use RateLimiter plus exponential retry, and respect any Retry-After header. Alert on 4xx rates and queue depth, not just 5xx — a silent stream of 401s from rotated credentials shows up as a sync gap, not an error page.

Auth expiry and credential rotation surface as silent sync gaps. Add a health check job that performs a cheap authenticated call (list count, or similar) on a schedule and alerts on failure.

Bulk syncs need idempotency keyed on the email hash. Retries will re-subscribe the same address; dedupe upstream or accept that FreshMail will handle it, but know which. Resumability is on you.

Synchronous sends will bite you. Any timeout in a request path is user-facing latency — keep sends queued even when it feels like overkill for "one welcome email."

Compliance is entirely out of scope. No unsubscribe handling, no consent capture, no suppression list, no GDPR/CAN-SPAM tooling. If lifecycle email is shipping, those are product requirements you own end to end, and they are audit-relevant.

No webhooks, bounces, complaints, or template management. If the roadmap needs deliverability tooling, this package is not even a starting point — it's a distraction. Go straight to evaluating replacement providers.

Licence: verify before forking. Likely MIT, but confirm. You will be vendoring or forking with no upstream, so the licence terms and internal redistribution policy matter.

Don't copy the old idioms. Expect pre-PSR-4-strict autoloading, missing types, and static state. Copy the request contracts and the signing recipe; write the surrounding Laravel code fresh with typed properties, readonly DTOs, and strict types.

Cache-safe config. Read credentials via config('services.freshmail.*'), never env() directly — php artisan config:cache will return null for raw env() calls in production and you'll spend an afternoon on a 401.

Support surface is internal only. No maintainer, no advisories, no forum. Write a short README for the ported client covering auth, endpoints used, retry semantics, and how to rotate credentials — and make sure someone other than the original implementer can read it. Budget a few days per year for API bumps and dependency CVEs.

Bottom line for day-to-day use: treat the repository as a read-only spec. One spike, one to two days of build, one day of tests, negligible ongoing cost — provided the vendor check passes. If FreshMail is being sunset, skip straight to replacement providers.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core