SendPortal Core is a Laravel package (mettle/sendportal-core) that provides self-hosted email marketing functionality. It can be embedded into any Laravel 10+ application or used via the standalone SendPortal wrapper.
- Namespace:
Sendportal\Base - License: MIT
- PHP: 8.2+
- Laravel: 10+
- Databases: MySQL (≥ 5.7) or PostgreSQL (≥ 9.4)
This is a Laravel service provider package. The entry point is SendportalBaseServiceProvider, which registers routes, views, migrations, events, config, and the sp:campaigns:dispatch scheduled command.
src/
├── Adapters/ # Mail provider adapters (SES, Sendgrid, Mailgun, Postmark, Mailjet, Postal, SMTP)
├── Console/Commands/ # Artisan commands (CampaignDispatchCommand)
├── Events/ # Domain events (MessageDispatch, SubscriberAdded, webhook events)
├── Exceptions/ # Exception handler
├── Facades/ # Sendportal and Helper facades
├── Factories/ # MailAdapterFactory
├── Http/
│ ├── Controllers/ # Web controllers (Campaigns, Subscribers, Tags, Messages, Templates, EmailServices)
│ │ └── Api/ # REST API controllers + webhook handlers
│ ├── Requests/ # Form request validation
│ └── Resources/ # API resources
├── Interfaces/ # Contracts (MailAdapterInterface, BaseTenantInterface, BaseEloquentInterface)
├── Listeners/ # Event listeners (message dispatch, webhook handlers)
├── Models/ # Eloquent models (Campaign, Subscriber, Tag, Message, Template, EmailService, etc.)
├── Pipelines/ # Campaign processing pipelines
├── Presenters/ # View presenters (CampaignReportPresenter)
├── Providers/ # Service providers (App, Event, Form, Route, Resolver)
├── Repositories/ # Data access layer (tenant-scoped and base repositories)
├── Routes/ # Route definitions (WebRoutes, ApiRoutes)
├── Rules/ # Validation rules
├── Services/ # Business logic (Campaigns, Messages, Subscribers, Tags, Templates, Webhooks, Content)
├── Traits/ # Shared traits (Uuid, ThrottlesSending, ScheduledAt, etc.)
└── View/Components/ # Blade components
- Multi-tenancy: Repositories are tenant-scoped via
BaseTenantRepository. Workspace ID is resolved throughResolverService. - Mail Adapters:
MailAdapterInterfacedefines the contract.MailAdapterFactoryresolves the correct adapter based onEmailServicemodel config. Supported: SES, Sendgrid, Mailgun, Postmark, Mailjet, Postal, SMTP. - Campaign Dispatch Flow:
sp:campaigns:dispatch(runs every minute) →CampaignDispatchService→MessageDispatchEvent→MessageDispatchHandler→DispatchMessage→ mail adaptersend(). - Webhook Processing: Each provider has a dedicated webhook controller, event, and listener under
Api/Webhooks,Events/Webhooks, andListeners/Webhooks.
Campaign, CampaignStatus, Subscriber, Tag, Subscription, Template, Message, MessageUrl, MessageFailure, EmailService, EmailServiceType, UnsubscribeEventType.
- Web routes:
WebRoutes::sendportalWebRoutes()(authenticated) andsendportalPublicWebRoutes()(subscriptions, webview). - API routes:
ApiRoutes::sendportalApiRoutes()(v1, authenticated) andsendportalPublicApiRoutes()(webhooks, ping). - Routes are registered via the
Sendportalfacade in the host application.
- Framework: PHPUnit 11
- Test base:
Tests\TestCaseextends Orchestra Testbench - Run:
composer test - CI tests against both MySQL and PostgreSQL on PHP 8.2 and 8.3
- Test directories:
tests/Feature/(Campaigns, Subscribers, Tags, Messages, Templates, EmailServices, Webhooks, Content, API) andtests/Unit/(Http, Factories, Services, Repositories, Models)
Published as config/sendportal.php. Keys:
command_output_path— output path for command logslist_unsubscribe.email/list_unsubscribe.url— List-Unsubscribe header values
- Add a new mail adapter: Implement
MailAdapterInterface, extendBaseMailAdapter, register inMailAdapterFactory, addEmailServiceTypemigration. - Add a new webhook provider: Create webhook controller in
Api/Webhooks, event inEvents/Webhooks, listener inListeners/Webhooks, register inEventServiceProvider, add route inApiRoutes. - Modify campaign logic: Business logic lives in
Services/Campaigns/. The dispatch pipeline is inPipelines/Campaigns/. - Add subscriber features: Controllers in
Http/Controllers/Subscribers/, services inServices/Subscribers/, repository inRepositories/Subscribers/.
Use the format: <type>: <short description in lowercase>
Types:
feat:— new feature or enhancementfix:— bug fixoptimize:— performance improvement
Examples:
feat: add API endpoint to send test email
fix: detach tags before deleting campaign
feat: cache campaigns stats
fix: campaign stats API pagination
feat: return unsubscribed datetime and event for subscribers API
Keep messages concise and descriptive of what changed, not how.