agavesoft/mailflow
MailFlow is a Laravel/PHP package for building expressive email workflows. Define message sequences, conditions, and delivery steps with a clean API, then integrate them into your app to automate transactional or scheduled mail flow.
What this release actually is. v2.5.0 is not a single increment — it is a bundled publication of four never-tagged releases (2.2.0 Factura Facilita attachments, 2.3.0 F-009 catalog/provisioning, 2.4.0 F-011 pull sync, 2.5.0 F-010 guaranteed events/outbox). From a program perspective, treat this as a platform feature wave, not a patch. Scoping, estimation, and QA must be sized against F-008/F-009/F-010/F-011 collectively.
Architecture fit. The package is a Laravel SDK for a hosted notification/contact platform, and the new release moves it from "thin API client" to "transactional participant in the app":
smartmailto_outbox, smartmailto_outbox_alert_state) makes the app the system of record for intent until the server acknowledges by key. This is the correct pattern for delivery guarantees and is a genuine architectural improvement — it converts fire-and-forget API calls into auditable, retryable units of work.POST {prefix}/smartmailto/pull) inverts data flow: the SaaS now calls into the Laravel app via Contracts\PullResolver. This makes the app an integration surface with an externally reachable, signed, throttled route — a new attack surface and a new uptime dependency.variables/*.yaml|yml|json), enabling CI-driven configuration. This is the highest-leverage change for a TPM: configuration becomes reviewable and testable rather than click-ops.Integration feasibility. Strong for a Laravel shop, with three concrete frictions:
vcs repository entry pointing at https://github.com/agavesoft/notificabot-lara-pkg-sdk with "agavesoft/smartmailto": "^2.5". Consequence: CI runners need GitHub credentials, composer audit/security advisory tooling does not cover this dependency, and there is no Packagist-mediated availability guarantee. Mitigation: vendor/fork with an internal mirror and tag discipline.smartmailto:provision 404s, the pull route does not register, outbox has nothing to talk to. SDK version and server version must be released as a coordinated pair — add this to the dependency/upgrade plan explicitly.symfony/yaml ^7.2|^8.0 plus illuminate/database|encryption|filesystem|mail. Symfony 7.2 implies PHP 8.2+. Confirm the minimum PHP and Laravel versions supported before committing; this may force an unrelated framework upgrade.Technical risk (ranked).
outbox.connection vs. the app's transaction. Notes state outbox rows are written "en la transacción de la app (conexión outbox.connection)". A single atomic transaction across two database connections is not possible in Laravel. If outbox.connection is configured to a separate database, the advertised transactional guarantee silently degrades to best-effort. Either pin outbox.connection to the app connection, or accept and document the weaker guarantee.APP_KEY rotation is now a data-loss event. Outbox payloads are encrypted with APP_KEY. Rotating the key can orphan in-flight rows mid-retry (up to 72 h). Key rotation must be gated on an empty/reconciled outbox — a new operational runbook item.gave_up. Alerting exists, but the design intentionally trades fast failure for resilience; teams must monitor smartmailto:outbox:status and the heartbeat, not just error rates.SMARTMAILTO_OUTBOX_ENABLED=false), pull (SMARTMAILTO_PULL_ENABLED=false). This makes rollout safe but also means a team can believe it has delivery guarantees while running the legacy synchronous path. Feature-flag state must be a tracked environment fact, not an implicit assumption.SmartmailtoOutboxFailed → needsEmergencySend() → app sends directly (e.g., SES) → reportExternalSend() loop is the only thing preventing double delivery. reportExternalSend throws OutboxRowInFlight if the row is still in flight, and marks superseded otherwise. This race must be implemented and tested deliberately, not improvised during an incident.(updatedAt, key) will silently skip records. This is the highest-probability correctness bug in the whole release.fromDisk() regenerates a signed URL per attempt (good); fromUrl() carries a caller-supplied sha256 and an external TTL, which must outlive the 72 h retry window.UploadedFi..."), and dates in 2026. No intermediate tags means no clean rollback to 2.4.0 and no bisect granularity. The F-008 attachment API surface is not fully documented.Key questions for the vendor/team.
outbox.connection intended to be the app connection? If not, what is the durability contract?SELECT ... FOR UPDATE SKIP LOCKED or an equivalent lease? Safe to run N replicas?track/identify — what is the recommended index and retention budget?X-Smartmailto-Request single-use enforcement backed by shared cache (multi-node safe) or per-node?Stack fit. Good if the platform is Laravel 11/12 on PHP 8.2+ with MySQL/Postgres and a queue/scheduler already in place. The package assumes: a working scheduler (smartmailto:outbox:work, heartbeat), a cache for replay protection, Storage disks for attachments, and encryption via APP_KEY. No queue worker integration is mentioned — the outbox worker is a standalone artisan command, so it needs its own supervisor/systemd unit or scheduler entry.
Migration path (recommended sequencing).
^2.5, confirm PHP/Laravel compatibility, and stand up an internal mirror or fork tag so CI does not depend on live GitHub access. Do this before any feature work.smartmailto:provision --validate into CI as the first win: it is read-only, fails loudly, and requires no runtime behavior change. This de-risks the whole adoption by proving catalogue/server connectivity early. Note validatePackage returns HTTP 200 even when invalid — CI must assert on the valid field, not the status code.php artisan vendor:publish --tag=smartmailto-migrations, inspect indexes on smartmailto_outbox (status, send_before, updated_at, key), and decide outbox.connection explicitly. Run in staging for at least one full 72 h retry cycle before production.alerts.mail_to / alerts.teams_webhook_url must be live before SMARTMAILTO_OUTBOX_ENABLED=true, otherwise the first failure is invisible. Confirm outbound egress to the heartbeat endpoint at the same time.sendBefore enforcement and audit callers. With outbox on, send() requires sendBefore; track() without occurredAt silently uses commit time; identify() sends call time. Grep every call site and add the parameters deliberately — otherwise analytics timestamps drift to "when the DB committed", which is a subtle but permanent data-quality change.OutboxRowInFlight, call reportExternalSend with channel/reason, and add SmartmailtoFake::assertExternalSendReported() coverage. Add a drill to the on-call playbook.SMARTMAILTO_PULL_SECRET (pullsec_..., distinct from the webhook secret), route prefix decision (api default → /api/smartmailto/pull), throttle 120,1 tuning, and a PullResolver implementation with stable (updatedAt, key) ordering and correct 5 MB continuation. Test exclusively through Smartmailto::fakePull($resolver) + assertPullContract() before exposing the real route.fromDisk (per-attempt signed URLs) for anything that may sit in the outbox, and cap inline payloads under 1 MB by policy.Compatibility notes. All four features are backward compatible with flags off, so a "deploy dark, enable in order" plan is viable. The only non-flag behavior change is the sendBefore requirement when outbox is enabled and the timestamp semantics that follow it. There is no deprecation of existing methods; health()-based direct sending is no longer recommended but still works.
Sequencing constraint. 2.4.0's pull cursor and identify(updatedAt:) only make sense alongside 2.5.0's outbox semantics — the bundled tag means you cannot stage 2.4.0 independently of 2.5.0 code. Plan the rollout by flag, not by version.
Maintenance. The app now owns schema (two tables), a background worker, a scheduled heartbeat, and an inbound route — a materially larger operational footprint than an HTTP client. Budget ongoing ownership: index bloat review, smartmailto:outbox:prune scheduling, secret rotation for two distinct secrets, and upgrade coordination with the server's F-0xx releases. Because 2.2.0–2.4.0 were never tagged, upgrades cannot be bisected; expect vendor escalations to be slower and pin by commit SHA if you need intermediate states.
Support and on-call. The alert model is well designed for humans: first alert at 15 min, reminders at 1 h / 4 h / 12 h / 24 h / 48 h, summary at 72 h, plus a "recovered" notice; 422s fire immediately; failures group per project per template in 15-minute windows. This is genuinely good TPM-leverage — it gives you an SLA-shaped escalation ladder out of the box. Two caveats: (a) grouped alerts reduce noise but can mask breadth — track "distinct templates failing" as a secondary signal; (b) Teams/email is the only channel, so it must map to a real rotation. smartmailto:outbox:status, retry {id*} --key= --failed, and prune give support a self-service remediation toolkit — document them in the runbook with exact invocations.
Scaling. Every track, send, identify, link, and reportExternalSend writes an outbox row when enabled. For a high-event-volume product, the outbox becomes one of the busiest write tables in the database and is on the critical path of user-facing transactions (you are adding a write inside the app transaction). Validate: rows/day, index size after prune, worker throughput, and whether multiple worker replicas are safe given the 10-minute sending lease reclaim. Pull-side scaling depends entirely on the resolver: history_months: null means "all history", which is a trap for large contact tables — set it explicitly and verify the 5 MB truncation path continues correctly.
Failure modes to pre-write. (1) APP_KEY rotation orphans in-flight rows. (2) Expired/rotated API token → 72 h silent backlog, then gave_up. (3) Emergency send without reportExternalSend → duplicate messages to customers. (4) Pull cursor instability → silently missing contacts, no error. (5) Attachment signed URL TTL shorter than the retry window. (6) Outbox flag off in an environment where the team assumes guarantees. (7) Clock skew between app and server corrupting last-write-wins resolution. Each needs a detection signal and a one-line remedy.
Compliance. prune --contact= plus per-person retention boundaries (acked 7 days, closed 30 days) indicates ARCO/data-subject-request support. SmartmailtoOutboxFailed deliberately excludes personal data — good for log hygiene. Confirm the 7/30-day defaults satisfy your own retention policy before shipping; they are short and are your only lever.
Ramp-up. Estimate roughly: 0.5 day supply-chain/compatibility, 1 day provisioning-in-CI, 2–3 days outbox including migration review and emergency path, 2–3 days pull including resolver correctness and 5 MB pagination, 1 day attachments. Add a dedicated half-day for the operational runbook (alerts, key rotation, worker supervision, remediation commands) — that artifact, not the code, is what determines whether this release is a success. The SmartmailtoFake surface (assertLinked, assertExternalSendReported, fakePull/assertPullContract, assertPackageProvisioned, assertVariablePut, assertActivated) is comprehensive enough that a team can build confidence without a live server, which materially shortens the ramp.
How can I help you explore Laravel packages today?