File: /home/xedaptot/be.naniguide.com/app/Http/Controllers/Refactor/SendingController.php
<?php
namespace App\Http\Controllers\Refactor;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Model\SendingServer;
use App\Model\SendingIdentity;
use App\Model\Sender;
use App\SendingServers\Identities\IdentityCreationService;
use App\SendingServers\Identities\IdentityKind;
use App\SendingServers\Identities\IdentityStatus;
use App\SendingServers\Identities\ManagementMode;
use App\SendingServers\DomainVerification\RecordPurpose;
use App\Model\TrackingDomain;
use App\Model\EmailVerificationServer;
use App\Model\Blacklist;
use App\Model\Signature;
use App\Model\Setting;
use App\Model\BounceHandler;
use App\Model\FeedbackLoopHandler;
use App\Library\Facades\Hook;
use App\Services\Customer\SendingDomainService;
use App\Services\Customer\SendingServerService;
use App\Services\Customer\VerificationServerService;
use App\Services\Plans\Entitlements\EntitlementKey;
use App\Services\Plans\Gates\EntitlementGate;
use App\Services\Plans\Gates\QuotaGate;
use App\Services\Plans\Quotas\QuotaKey;
use App\Services\Plans\Quotas\QuotasService;
use Gate;
class SendingController extends Controller
{
// ─── Sending Servers ────────────────────────────────────────
public function servers(Request $request, SendingServerService $service)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
if (!$customer->can('read', new SendingServer())) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.servers', [
'customer' => $customer,
'meta' => $service->headerMeta($customer),
'sidebar' => $service->sidebarStats($customer),
]);
}
public function serversListing(Request $request, SendingServerService $service)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
if (!$customer->can('read', new SendingServer())) {
return $this->notAuthorized();
}
$servers = $service->list(
$customer,
$request->input('keyword') ?: null,
$request->input('status') ?: null,
$request->input('type') ?: null,
(string) ($request->input('sort_order') ?: 'created_at'),
(string) ($request->input('sort_direction') ?: 'desc'),
(int) ($request->input('per_page') ?: 25),
);
return view('refactor.pages.sending._servers_list', [
'servers' => $servers,
]);
}
// ─── Sending Domains ────────────────────────────────────────
public function domains(Request $request, SendingDomainService $service)
{
$customer = $request->user()->customer;
if (!$service->customerCanVerify($customer)) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.domains', [
'customer' => $customer,
'stats' => $service->dashboardStats($customer),
'sidebar' => $service->sidebarStats($customer),
]);
}
public function domainsListing(Request $request, SendingDomainService $service)
{
$customer = $request->user()->customer;
if (!$service->customerCanVerify($customer)) {
return $this->notAuthorized();
}
$domains = $service->list(
$customer,
$request->input('keyword') ?: null,
$request->input('status') ?: null,
(string) ($request->input('sort_order') ?: 'created_at'),
(string) ($request->input('sort_direction') ?: 'desc'),
(int) ($request->input('per_page') ?: 25),
);
// Plan's primary system server — drives the per-row "usable?" badge.
// null when plan uses customer's own server or there's no subscription.
$subscription = $customer->getCurrentActiveSubscription();
$plan = $subscription?->plan;
$primaryServer = ($plan && $plan->useSystemSendingServer())
? $plan->primarySendingServer()
: null;
return view('refactor.pages.sending._domains_list', [
'domains' => $domains,
'primaryServer' => $primaryServer,
]);
}
// ─── Verified Senders ───────────────────────────────────────
public function senders(Request $request)
{
return redirect()->route('refactor.sending.domains');
$customer = $request->user()->customer;
$plan = $customer->getCurrentActiveSubscription()?->plan;
if (!$plan || !$plan->allowSenderVerification()) {
return $this->notAuthorized();
}
$total = Sender::where('customer_id', $customer->id)->count();
$verified = Sender::where('customer_id', $customer->id)->where('status', Sender::STATUS_VERIFIED)->count();
return view('refactor.pages.sending.senders', [
'customer' => $customer,
'total' => $total,
'verified' => $verified,
]);
}
public function sendersListing(Request $request)
{
$customer = $request->user()->customer;
$request->merge(['customer_id' => $customer->id]);
$senders = Sender::search($request)->paginate($request->per_page ?? 25);
return view('refactor.pages.sending._senders_list', [
'senders' => $senders,
]);
}
// ─── Tracking Domains ───────────────────────────────────────
/**
* Feature gate for the Tracking Domains screens. MAX_TRACKING_DOMAINS quota
* acts as both the access switch and the resource cap:
* - limit === 0 → feature disabled on plan (also: no subscription)
* - limit === null → unlimited
* - limit > 0 → access granted, cap enforced at create time
*/
private function trackingDomainsAccessible(\App\Model\Customer $customer): bool
{
$limit = app(QuotasService::class)->limit($customer, QuotaKey::MAX_TRACKING_DOMAINS);
return $limit === null || $limit > 0;
}
public function trackingDomains(Request $request)
{
$customer = $request->user()->customer;
if (!$this->trackingDomainsAccessible($customer)) {
return $this->notAuthorized();
}
$total = $customer->local()->trackingDomains()->count();
$verified = $customer->local()->trackingDomains()->where('status', TrackingDomain::STATUS_VERIFIED)->count();
return view('refactor.pages.sending.tracking_domains', [
'customer' => $customer,
'total' => $total,
'verified' => $verified,
]);
}
public function trackingDomainsListing(Request $request)
{
$customer = $request->user()->customer;
if (!$this->trackingDomainsAccessible($customer)) {
return $this->notAuthorized();
}
$request->merge(['customer_id' => $customer->id]);
$domains = TrackingDomain::search($request)->paginate($request->per_page ?? 25);
return view('refactor.pages.sending._tracking_domains_list', [
'domains' => $domains,
]);
}
public function trackingDomainCreate(Request $request)
{
$customer = $request->user()->customer;
if (!QuotaGate::allows($customer, QuotaKey::MAX_TRACKING_DOMAINS, amount: 1)) {
return $this->notAuthorized();
}
$domain = new TrackingDomain(['scheme' => 'https']);
$domain->status = TrackingDomain::STATUS_UNVERIFIED;
$domain->uid = '0';
return view('refactor.pages.sending.tracking_domain_form', [
'domain' => $domain,
'isCreate' => true,
]);
}
public function trackingDomainStore(Request $request)
{
$customer = $request->user()->customer;
if (!QuotaGate::allows($customer, QuotaKey::MAX_TRACKING_DOMAINS, amount: 1)) {
return $this->notAuthorized();
}
[$domain, $validator] = TrackingDomain::createFromRequest($request);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
}
return redirect()->route('refactor.sending.tracking_domain_verify', $domain->uid)
->with('alert-success', trans('refactor/sending.tracking.flash_created'));
}
public function trackingDomainEdit(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!$this->trackingDomainsAccessible($customer)) {
return $this->notAuthorized();
}
$domain = TrackingDomain::findByUid($uid);
return view('refactor.pages.sending.tracking_domain_form', [
'domain' => $domain,
'isCreate' => false,
]);
}
public function trackingDomainUpdate(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!$this->trackingDomainsAccessible($customer)) {
return $this->notAuthorized();
}
$domain = TrackingDomain::findByUid($uid);
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'port' => 'nullable|integer|between:1,65535',
]);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
}
[$name, $port] = TrackingDomain::parseNameAndPortFromRequest($request);
$domain->scheme = $request->input('scheme', 'http');
$domain->name = $name;
$domain->port = $port;
$domain->verification_method = $request->input('verification_method', TrackingDomain::VERIFICATION_METHOD_HOST);
$domain->status = TrackingDomain::STATUS_UNVERIFIED;
$domain->save();
return redirect()->route('refactor.sending.tracking_domain_verify', $domain->uid)
->with('alert-success', trans('refactor/sending.tracking.flash_updated'));
}
public function trackingDomainVerify(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!$this->trackingDomainsAccessible($customer)) {
return $this->notAuthorized();
}
$domain = TrackingDomain::findByUid($uid);
$hostname = parse_url(url('/'), PHP_URL_HOST);
return view('refactor.pages.sending.tracking_domain_verify', [
'domain' => $domain,
'hostname' => $hostname,
'caddyServerHostname' => config('caddy.server_hostname', $hostname),
'isAutossl' => config('caddy.autossl') == true,
'downloadUrl' => url('tracking-1.0.0.zip'),
]);
}
public function trackingDomainCheck(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!$this->trackingDomainsAccessible($customer)) {
return response()->json(['success' => false], 403);
}
$domain = TrackingDomain::findByUid($uid);
$domain->verify();
return response()->json([
'success' => $domain->isVerified(),
'status' => $domain->status,
'message' => $domain->isVerified()
? trans('refactor/sending.tracking.flash_verified')
: trans('refactor/sending.tracking.verify_failed'),
]);
}
// ─── Email Verification Servers ─────────────────────────────
public function verificationServers(Request $request, VerificationServerService $service)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_EMAIL_VERIFICATION_SERVER)) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.verification_servers', [
'customer' => $customer,
'meta' => $service->headerMeta($customer),
'sidebar' => $service->sidebarStats($customer),
]);
}
public function verificationServersListing(Request $request, VerificationServerService $service)
{
$customer = $request->user()->customer;
$servers = $service->list(
$customer,
$request->input('keyword') ?: null,
$request->input('status') ?: null,
(string) ($request->input('sort_order') ?: 'created_at'),
(string) ($request->input('sort_direction') ?: 'desc'),
(int) ($request->input('per_page') ?: 25),
);
return view('refactor.pages.sending._verification_servers_list', [
'servers' => $servers,
]);
}
// ─── Blacklist ──────────────────────────────────────────────
public function blacklist(Request $request)
{
$customer = $request->user()->customer;
$total = $customer->blacklists()->count();
return view('refactor.pages.sending.blacklist', [
'customer' => $customer,
'total' => $total,
]);
}
public function blacklistListing(Request $request)
{
$customer = $request->user()->customer;
$request->merge(['customer_id' => $customer->id]);
$blacklists = Blacklist::search($request)->paginate($request->per_page ?? 25);
return view('refactor.pages.sending._blacklist_list', [
'blacklists' => $blacklists,
]);
}
public function blacklistDelete(Request $request)
{
$customer = $request->user()->customer;
if ($request->select_tool == 'all_items') {
$blacklists = $customer->blacklists();
} else {
$blacklists = Blacklist::whereIn(
'id',
is_array($request->uids) ? $request->uids : explode(',', $request->uids)
);
}
$deleted = 0;
foreach ($blacklists->get() as $blacklist) {
if ($customer->can('delete', $blacklist)) {
$blacklist->delist($customer);
$blacklist->delete();
$deleted++;
}
}
return response()->json([
'message' => $deleted . ' ' . trans('refactor/sending.blacklist.flash_deleted'),
]);
}
public function blacklistStore(Request $request)
{
$customer = $request->user()->customer;
if (!$customer->can('create', new Blacklist())) {
return response()->json(['status' => 'error', 'message' => 'Not authorized'], 403);
}
$validator = \Validator::make($request->all(), [
'email' => 'required|email',
]);
if ($validator->fails()) {
return response()->json(['status' => 'error', 'errors' => $validator->errors()], 422);
}
$blacklist = new Blacklist();
$blacklist->fill($request->only(['email', 'reason']));
$blacklist->customer_id = $customer->id;
$blacklist->save();
// Activity log not tracked for blacklist entries
return response()->json([
'status' => 'success',
'message' => trans('refactor/sending.blacklist.flash_added'),
]);
}
public function blacklistImport(Request $request)
{
$customer = $request->user()->customer;
$job = $customer->importBlacklistJobs()->first();
return view('refactor.pages.sending.blacklist_import', [
'customer' => $customer,
'job' => $job,
]);
}
public function blacklistStartImport(Request $request)
{
$customer = $request->user()->customer;
if (!$request->hasFile('file')) {
return response()->json(['status' => 'error', 'message' => 'No file uploaded'], 422);
}
$filepath = Blacklist::upload($request->file('file'));
$job = $customer->dispatchWithMonitor(new \App\Jobs\ImportBlacklistJob($filepath, $customer));
return response()->json([
'status' => 'success',
'message' => trans('refactor/sending.blacklist.import_started'),
'job_uid' => $job->uid,
]);
}
public function blacklistImportProgress(Request $request, $job_uid)
{
$job = \App\Model\JobMonitor::findByUid($job_uid);
if (!$job) {
return response()->json(['status' => 'error', 'message' => 'Job not found'], 404);
}
$progress = $job->getJsonData();
$progress['status'] = $job->status;
$progress['error'] = $job->error;
return response()->json($progress);
}
public function blacklistCancelImport(Request $request, $job_uid)
{
$job = \App\Model\JobMonitor::findByUid($job_uid);
if (!$job) {
return response()->json(['status' => 'error', 'message' => 'Job not found'], 404);
}
$job->cancel();
return response()->json(['status' => 'success']);
}
// ─── Signatures ─────────────────────────────────────────────
public function signatures(Request $request)
{
if (Setting::get('signature.enabled') != 'yes') {
return $this->notAuthorized();
}
$customer = $request->user()->customer;
$total = $customer->signatures()->count();
return view('refactor.pages.sending.signatures', [
'customer' => $customer,
'total' => $total,
]);
}
public function signaturesListing(Request $request)
{
$customer = $request->user()->customer;
$signatures = $customer->signatures()
->search($request->keyword)
->orderBy($request->sort_order ?? 'created_at', $request->sort_direction ?? 'asc')
->paginate($request->per_page ?? 25);
return view('refactor.pages.sending._signatures_list', [
'signatures' => $signatures,
]);
}
public function signaturesDelete(Request $request)
{
$uids = is_array($request->uids) ? $request->uids : explode(',', $request->uids);
$deleted = 0;
foreach (Signature::whereIn('uid', $uids)->get() as $item) {
if ($request->user()->customer->can('delete', $item)) {
$item->delete();
$deleted++;
}
}
return response()->json(['message' => $deleted . ' ' . trans('refactor/sending.signatures.flash_deleted')]);
}
public function signatureCreate(Request $request)
{
if (Setting::get('signature.enabled') != 'yes') {
return $this->notAuthorized();
}
$customer = $request->user()->customer;
if (!$customer->can('create', Signature::class)) {
return $this->notAuthorized();
}
$signature = $customer->local()->newSignature();
return view('refactor.pages.sending.signature_form', [
'mode' => 'create',
'signature' => $signature,
]);
}
public function signatureStore(Request $request)
{
if (Setting::get('signature.enabled') != 'yes') {
return $this->notAuthorized();
}
$customer = $request->user()->customer;
if (!$customer->can('create', Signature::class)) {
return $this->notAuthorized();
}
$signature = $customer->local()->newSignature();
[$result, $errors] = $signature->saveSignature(
$request->name,
$request->content,
(bool) $request->is_default,
);
if (!$result) {
return back()->withErrors($errors)->withInput();
}
return redirect()->route('refactor.sending.signatures')
->with('alert-success', trans('refactor/sending.signatures.flash_created', ['name' => $signature->name]));
}
public function signatureEdit(Request $request, $uid)
{
if (Setting::get('signature.enabled') != 'yes') {
return $this->notAuthorized();
}
$customer = $request->user()->customer;
$signature = Signature::findByUid($uid);
if (!$signature || !$customer->can('update', $signature)) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.signature_form', [
'mode' => 'edit',
'signature' => $signature,
]);
}
public function signatureUpdate(Request $request, $uid)
{
if (Setting::get('signature.enabled') != 'yes') {
return $this->notAuthorized();
}
$customer = $request->user()->customer;
$signature = Signature::findByUid($uid);
if (!$signature || !$customer->can('update', $signature)) {
return $this->notAuthorized();
}
[$result, $errors] = $signature->saveSignature(
$request->name,
$request->content,
(bool) $request->is_default,
);
if (!$result) {
return back()->withErrors($errors)->withInput();
}
return redirect()->route('refactor.sending.signatures')
->with('alert-success', trans('refactor/sending.signatures.flash_updated', ['name' => $signature->name]));
}
public function signatureSetDefault(Request $request, $uid)
{
$customer = $request->user()->customer;
$signature = Signature::findByUid($uid);
if (!$signature || !$customer->can('update', $signature)) {
return response()->json(['status' => 'error', 'message' => trans('messages.not_authorized')], 403);
}
$signature->setDefault();
return response()->json([
'status' => 'success',
'message' => trans('refactor/sending.signatures.flash_set_default', ['name' => $signature->name]),
]);
}
public function signatureEnable(Request $request, $uid)
{
$customer = $request->user()->customer;
$signature = Signature::findByUid($uid);
if (!$signature || !$customer->can('enable', $signature)) {
return response()->json(['status' => 'error', 'message' => trans('messages.not_authorized')], 403);
}
$signature->enable();
return response()->json([
'status' => 'success',
'message' => trans('refactor/sending.signatures.flash_enabled', ['name' => $signature->name]),
]);
}
public function signatureDisable(Request $request, $uid)
{
$customer = $request->user()->customer;
$signature = Signature::findByUid($uid);
if (!$signature || !$customer->can('disable', $signature)) {
return response()->json(['status' => 'error', 'message' => trans('messages.not_authorized')], 403);
}
$signature->disable();
return response()->json([
'status' => 'success',
'message' => trans('refactor/sending.signatures.flash_disabled', ['name' => $signature->name]),
]);
}
// ─── Generic Delete Helpers ─────────────────────────────────
public function serversDelete(Request $request)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$uids = is_array($request->uids) ? $request->uids : explode(',', $request->uids);
$deleted = 0;
foreach (SendingServer::whereIn('uid', $uids)->get() as $item) {
if ($customer->can('delete', $item)) {
$item->delete();
$deleted++;
}
}
return response()->json(['message' => $deleted . ' ' . trans('refactor/sending.servers.flash_deleted')]);
}
public function domainsDelete(Request $request)
{
$uids = is_array($request->uids) ? $request->uids : explode(',', $request->uids);
$deleted = 0;
foreach (SendingIdentity::whereIn('uid', $uids)->get() as $item) {
if ($request->user()->customer->can('delete', $item)) {
$item->delete();
$deleted++;
}
}
return response()->json(['message' => $deleted . ' ' . trans('refactor/sending.domains.flash_deleted')]);
}
public function domainCreate(Request $request)
{
$customer = $request->user()->customer;
if (!$customer->can('create', SendingIdentity::class)) {
return $this->notAuthorized();
}
// Build a transient model just for the form view.
$domain = new SendingIdentity([
'kind' => IdentityKind::DOMAIN,
'management_mode' => ManagementMode::LOCAL_DKIM,
'status' => IdentityStatus::PENDING,
'signing_enabled' => true,
]);
$domain->uid = '0';
return view('refactor.pages.sending.domain_form', [
'domain' => $domain,
'mode' => 'create',
]);
}
public function domainStore(Request $request, IdentityCreationService $creator)
{
$customer = $request->user()->customer;
if (!$customer->can('create', SendingIdentity::class)) {
return response()->json(['status' => 'error', 'message' => trans('messages.not_authorized')], 403);
}
// Plan quota gate — refuse if customer is at or over MAX_SENDING_DOMAINS.
// Distinguish "feature not on plan" (false) from "feature on plan but cap reached"
// (int 0 = exhausted, or int N = used >= N) — they need different upsell copy.
$quotas = app(\App\Services\Plans\Quotas\QuotasService::class);
if (!$quotas->canCreate($customer, \App\Services\Plans\Quotas\QuotaKey::MAX_SENDING_DOMAINS)) {
$limit = $quotas->limit($customer, \App\Services\Plans\Quotas\QuotaKey::MAX_SENDING_DOMAINS);
if ($limit === false) {
// Plan does not grant MAX_SENDING_DOMAINS at all.
$msg = trans('refactor/sending.domains.not_on_plan');
} else {
// Plan grants it but cap reached (0 = exhausted, or N where used >= N).
$used = $quotas->used($customer, \App\Services\Plans\Quotas\QuotaKey::MAX_SENDING_DOMAINS);
$msg = trans('refactor/sending.domains.quota_exceeded', [
'used' => $used,
'limit' => $limit ?? '∞', // $limit is int here (incl 0); null already returned true from canCreate
]);
}
return response()->json([
'status' => 'error',
'message' => $msg,
'errors' => ['name' => [$msg]],
], 422);
}
$name = trim((string) $request->input('name', ''));
$validator = \Validator::make(
['name' => $name],
['name' => ['required', 'regex:' . SendingIdentity::VALIDATION_REGEXP]],
);
if ($validator->fails()) {
return response()->json(['status' => 'error', 'errors' => $validator->errors()], 422);
}
// The Sending Domains page is the entry point for LOCAL_DKIM identities
// (server-less, Acelle-signed). If the customer's plan binds a vendor
// server with SupportsRemoteDomainVerify, also push to that vendor.
$subscription = $customer->getCurrentActiveSubscription();
$plan = $subscription ? $subscription->plan : null;
$vendorServer = ($plan && $plan->useSystemSendingServer()) ? $plan->primarySendingServer() : null;
$useVendor = $vendorServer
&& $vendorServer->driver() instanceof \App\SendingServers\Capabilities\SupportsRemoteDomainVerify;
$value = strtolower($name);
$existingQuery = SendingIdentity::query()
->where('kind', IdentityKind::DOMAIN->value)
->where('value', $value);
if ($useVendor) {
$existingQuery->where('sending_server_id', $vendorServer->id);
} else {
$existingQuery->whereNull('sending_server_id')
->where('customer_id', $customer->local()->id);
}
$existing = $existingQuery->first();
if ($existing) {
$ownerUid = $existing->customer?->getAttribute('uid');
$msg = $ownerUid
? trans('refactor/sending.domains.flash_duplicate_with_owner', ['uid' => $ownerUid])
: trans('refactor/sending.domains.flash_duplicate');
return response()->json([
'status' => 'error',
'errors' => ['name' => [$msg]],
], 422);
}
try {
$domain = $creator->create(
server: $useVendor ? $vendorServer : null,
kind: IdentityKind::DOMAIN,
value: $value,
customer: $customer->local(),
);
} catch (\Illuminate\Database\QueryException $e) {
if (($e->errorInfo[1] ?? null) === 1062) {
return response()->json([
'status' => 'error',
'errors' => ['name' => [trans('refactor/sending.domains.flash_duplicate')]],
], 422);
}
throw $e;
} catch (\InvalidArgumentException $e) {
if (str_starts_with($e->getMessage(), 'Duplicate server-less identity')) {
return response()->json([
'status' => 'error',
'errors' => ['name' => [trans('refactor/sending.domains.flash_duplicate')]],
], 422);
}
throw $e;
}
return response()->json([
'status' => 'success',
'message' => trans('refactor/sending.domains.flash_created'),
'redirect' => route('refactor.sending.domain_show', $domain->uid),
]);
}
public function domainShow(Request $request, $uid)
{
$domain = SendingIdentity::where('uid', $uid)->first();
if (!$domain) {
abort(404);
}
$customer = $request->user()->customer;
if (!$customer->can('read', $domain)) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.domain_show', [
'domain' => $domain,
]);
}
// No domainEdit / domainUpdate — sending domains can't be modified post-
// creation. The `value` is bound to the locally-generated DKIM keypair
// and the customer's DNS records; editing would silently break the chain.
// To change: delete and re-add.
public function domainVerify(Request $request, $uid)
{
$domain = SendingIdentity::where('uid', $uid)->first();
if (!$domain) {
return response()->json(['status' => 'error', 'message' => 'Not found'], 404);
}
$domain->verify();
$verified = $domain->status === IdentityStatus::VERIFIED;
return response()->json([
'status' => 'success',
'verified' => $verified,
'message' => $verified
? trans('refactor/sending.domains.verify_success')
: trans('refactor/sending.domains.verify_pending'),
]);
}
public function domainRecords(Request $request, $uid)
{
$domain = SendingIdentity::where('uid', $uid)->first();
if (!$domain) {
abort(404);
}
// Group records by purpose for the legacy view shape.
$identity = ($r = ($domain->recordsByPurpose(RecordPurpose::OWNERSHIP)[0] ?? null)) ? $r->toArray() : null;
$dkims = array_map(fn ($r) => $r->toArray(), $domain->recordsByPurpose(RecordPurpose::DKIM));
$spf = ($r = ($domain->recordsByPurpose(RecordPurpose::SPF)[0] ?? null)) ? $r->toArray() : null;
$dmarc = ($r = ($domain->recordsByPurpose(RecordPurpose::DMARC)[0] ?? null)) ? $r->toArray() : null;
Hook::collect('filter_aws_ses_dns_records', [&$identity, &$dkims, &$spf]);
return view('refactor.pages.sending._domain_records', [
'domain' => $domain,
'identity' => $identity,
'dkims' => $dkims,
'spf' => $spf,
'dmarc' => $dmarc,
]);
}
public function sendersDelete(Request $request)
{
$uids = is_array($request->uids) ? $request->uids : explode(',', $request->uids);
$deleted = 0;
foreach (Sender::whereIn('uid', $uids)->get() as $item) {
if ($request->user()->customer->can('delete', $item)) {
$item->delete();
$deleted++;
}
}
return response()->json(['message' => $deleted . ' ' . trans('refactor/sending.senders.flash_deleted')]);
}
public function senderCreate(Request $request)
{
$customer = $request->user()->customer;
if (Gate::denies('create', new Sender())) {
return $this->notAuthorized();
}
$sender = new Sender();
return view('refactor.pages.sending.sender_form', [
'sender' => $sender,
'mode' => 'create',
]);
}
public function senderStore(Request $request)
{
$customer = $request->user()->customer;
if (Gate::denies('create', new Sender())) {
return response()->json(['status' => 'error', 'message' => trans('messages.not_authorized')], 403);
}
$sender = new Sender();
$sender->fill($request->all());
$sender->customer_id = $customer->id;
$sender->status = Sender::STATUS_PENDING;
$validator = \Validator::make($request->all(), $sender->rules());
if ($validator->fails()) {
return response()->json(['status' => 'error', 'errors' => $validator->errors()], 422);
}
$sender->save();
// Determine sending server for verification
$server = null;
$plan = $customer->getCurrentActiveSubscription()?->plan;
if ($plan && $plan->useSystemSendingServer()) {
$server = $plan->primarySendingServer();
}
try {
$sender->verifyWith($server);
} catch (\Exception $e) {
// Verification email may fail but sender is saved
}
return response()->json([
'status' => 'success',
'message' => trans('refactor/sending.senders.flash_created'),
'redirect' => route('refactor.sending.sender_show', $sender->uid),
]);
}
public function senderShow(Request $request, $uid)
{
$sender = Sender::findByUid($uid);
if (!$sender) {
abort(404);
}
$customer = $request->user()->customer;
if (!$customer->can('read', $sender)) {
return $this->notAuthorized();
}
// Refresh verification status — remote SES/SMTP call may fail (network, credentials).
// Soft-failure: page still renders with last-known status. Log so ops can spot patterns.
try {
$sender->updateVerificationStatus();
} catch (\Throwable $e) {
\Log::warning('Sender verification refresh failed', [
'sender_id' => $sender->id,
'sender_email' => $sender->email,
'error' => $e->getMessage(),
]);
}
return view('refactor.pages.sending.sender_show', [
'sender' => $sender,
]);
}
public function senderEdit(Request $request, $uid)
{
$sender = Sender::findByUid($uid);
if (!$sender) {
abort(404);
}
$customer = $request->user()->customer;
if (!$customer->can('update', $sender)) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.sender_form', [
'sender' => $sender,
'mode' => 'edit',
]);
}
public function senderUpdate(Request $request, $uid)
{
$sender = Sender::findByUid($uid);
if (!$sender) {
return response()->json(['status' => 'error', 'message' => 'Not found'], 404);
}
$customer = $request->user()->customer;
if (!$customer->can('update', $sender)) {
return response()->json(['status' => 'error', 'message' => trans('messages.not_authorized')], 403);
}
$validator = \Validator::make($request->all(), $sender->editRules());
if ($validator->fails()) {
return response()->json(['status' => 'error', 'errors' => $validator->errors()], 422);
}
$sender->name = $request->name;
$sender->save();
return response()->json([
'status' => 'success',
'message' => trans('refactor/sending.senders.flash_updated'),
'redirect' => route('refactor.sending.sender_show', $sender->uid),
]);
}
public function trackingDomainsDelete(Request $request)
{
$uids = is_array($request->uids) ? $request->uids : explode(',', $request->uids);
$deleted = 0;
foreach (TrackingDomain::whereIn('uid', $uids)->get() as $item) {
if ($request->user()->customer->can('delete', $item)) {
$item->delete();
$deleted++;
}
}
return response()->json(['message' => $deleted . ' ' . trans('refactor/sending.tracking.flash_deleted')]);
}
public function verificationServersDelete(Request $request)
{
$uids = is_array($request->uids) ? $request->uids : explode(',', $request->uids);
$deleted = 0;
foreach (EmailVerificationServer::whereIn('uid', $uids)->get() as $item) {
if ($request->user()->customer->can('delete', $item)) {
$item->delete();
$deleted++;
}
}
return response()->json(['message' => $deleted . ' ' . trans('refactor/sending.verification_servers.flash_deleted')]);
}
public function verificationServerCreate(Request $request)
{
$server = new EmailVerificationServer();
$server->status = EmailVerificationServer::STATUS_ACTIVE;
if (!$request->user()->customer->can('create', $server)) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.verification_server_form', [
'server' => $server,
'services' => $this->buildVerificationServicesList(),
'options' => [],
'isEdit' => false,
]);
}
/**
* Build the {id, name, fields[]} list the form JS expects, from DriverRegistry.
* Replaces legacy config('verification.services') shape.
*/
private function buildVerificationServicesList(): array
{
$services = [];
foreach (\App\Verification\DriverRegistry::all() as $type => $driverClass) {
$cols = $driverClass::validationRules()['cols'] ?? [];
// Vendor-specific fields only; rate-limit fields are rendered separately.
$fields = array_values(array_filter(array_keys($cols), fn ($k) => !str_starts_with($k, 'limit_')));
$services[] = [
'id' => $type,
'name' => $driverClass::displayName(),
'fields' => $fields,
];
}
return $services;
}
public function verificationServerStore(Request $request)
{
$server = new EmailVerificationServer();
if (!$request->user()->customer->can('create', $server)) {
return $this->notAuthorized();
}
$server->fill($request->all());
$validator = \Validator::make($request->all(), $server->rules());
if ($validator->fails()) {
return response()->json(['status' => 'error', 'errors' => $validator->errors()], 422);
}
$server->customer_id = $request->user()->customer->id;
$server->status = EmailVerificationServer::STATUS_ACTIVE;
$server->options = $request->options ?? []; // $casts['options' => 'array'] handles JSON encoding
$server->save();
(new \App\ActivityLog\Activities\EmailVerificationServerCreated($server, $request->user()))->record();
return response()->json([
'status' => 'success',
'message' => trans('messages.email_verification_server.created'),
'redirect' => route('refactor.sending.verification_servers'),
]);
}
public function verificationServerEdit(Request $request, $uid)
{
$server = EmailVerificationServer::findByUid($uid);
if (!$request->user()->customer->can('update', $server)) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.verification_server_form', [
'server' => $server,
'services' => $this->buildVerificationServicesList(),
'options' => $server->options ?? [],
'isEdit' => true,
]);
}
public function verificationServerUpdate(Request $request, $uid)
{
$server = EmailVerificationServer::findByUid($uid);
if (!$request->user()->customer->can('update', $server)) {
return $this->notAuthorized();
}
$server->fill($request->all());
$validator = \Validator::make($request->all(), $server->rules());
if ($validator->fails()) {
return response()->json(['status' => 'error', 'errors' => $validator->errors()], 422);
}
$server->options = $request->options ?? []; // $casts['options' => 'array'] handles JSON encoding
$server->save();
(new \App\ActivityLog\Activities\EmailVerificationServerUpdated($server, $request->user()))->record();
return response()->json([
'status' => 'success',
'message' => trans('messages.email_verification_server.updated'),
'redirect' => route('refactor.sending.verification_server_edit', $server->uid),
]);
}
public function verificationServerEnable(Request $request, $uid)
{
$server = EmailVerificationServer::findByUid($uid);
if (!$request->user()->customer->can('enable', $server)) {
return response()->json(['status' => 'error', 'message' => 'Not authorized'], 403);
}
$server->enable();
return response()->json([
'status' => 'success',
'message' => trans('messages.email_verification_servers.enabled'),
]);
}
public function verificationServerDisable(Request $request, $uid)
{
$server = EmailVerificationServer::findByUid($uid);
if (!$request->user()->customer->can('disable', $server)) {
return response()->json(['status' => 'error', 'message' => 'Not authorized'], 403);
}
$server->disable();
return response()->json([
'status' => 'success',
'message' => trans('messages.email_verification_servers.disabled'),
]);
}
// ─── Server CRUD ────────────────────────────────────────────
public function serverSelect(Request $request)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
if (!$customer->can('create', SendingServer::class)) {
return $this->notAuthorized();
}
$types = $customer->getSendingServertypes();
// Plugin-registered types come in via the same `register_sending_server_driver`
// hook used by core. Picker metadata (name/description/icon_url) lives on
// the hook payload — plugins can't push translations into the main app's
// lang file, so the view renders these fields directly instead of through
// trans('refactor/sending.servers.type.X').
$pluginMeta = [];
foreach (\App\Library\Facades\Hook::collect('register_sending_server_driver') as $meta) {
if (!is_array($meta) || !isset($meta['type']) || !isset($meta['name'])) {
continue;
}
$pluginMeta[$meta['type']] = [
'name' => (string) $meta['name'],
'description' => (string) ($meta['description'] ?? ''),
'icon_url' => (string) ($meta['icon_url'] ?? ''),
];
}
return view('refactor.pages.sending.server_select', [
'types' => $types,
'pluginMeta' => $pluginMeta,
]);
}
public function serverCreate(Request $request)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
if (!$customer->can('create', SendingServer::class)) {
return $this->notAuthorized();
}
$type = $request->type;
if (!array_key_exists($type, $customer->getSendingServertypes())) {
abort(403);
}
$server = SendingServer::newOfType($type);
return view('refactor.pages.sending.server_create', [
'server' => $server,
'type' => $type,
'awsRegions' => SendingServer::awsRegionSelectOptions(),
'mailgunRegions' => SendingServer::mailgunRegionSelectOptions(),
]);
}
public function serverStore(Request $request)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
if (!$customer->can('create', SendingServer::class)) {
return $this->notAuthorized();
}
[$validator, $server] = SendingServer::createFromArray(array_merge($request->all(), [
'customer_id' => $customer->id,
]));
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
return redirect()->route('refactor.sending.server_edit', $server->uid)
->with('success', trans('refactor/sending.servers.created'));
}
public function serverEdit(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$server = $customer->sendingServers()->where('uid', $uid)->firstOrFail();
if (!$customer->can('update', $server)) {
return $this->notAuthorized();
}
return view('refactor.pages.sending.server_edit', [
'server' => $server,
'bounceHandlers' => BounceHandler::getSelectOptions(),
'feedbackHandlers' => FeedbackLoopHandler::getSelectOptions(),
'awsRegions' => SendingServer::awsRegionSelectOptions(),
'mailgunRegions' => SendingServer::mailgunRegionSelectOptions(),
'sendingLimitOptions' => $server->getSendingLimitSelectOptions(),
]);
}
/**
* Save the Connection card on the customer-side sending server edit page —
* driver-specific credentials (host, smtp_*, api_key, etc).
*/
public function serverConnectionSave(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$server = $customer->sendingServers()->where('uid', $uid)->firstOrFail();
if (!$customer->can('update', $server)) {
return $this->notAuthorized();
}
$validator = \Validator::make($request->all(), $server->getRules());
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
$server->fill($request->all());
$server->save();
return response()->json(['message' => trans('refactor/sending.servers.updated')]);
}
/**
* Save the Configuration card on the customer-side sending server edit page —
* name, default_from_email, handler IDs, sending_limit + quota_*.
* Customer-side has no Identity/Warmup tabs (admin-only), so this scope
* covers all non-Connection settings the customer can edit.
*/
public function serverConfigurationSave(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$server = $customer->sendingServers()->where('uid', $uid)->firstOrFail();
if (!$customer->can('update', $server)) {
return $this->notAuthorized();
}
$validator = \Validator::make($request->all(), $server->getConfigRules());
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
$server->fill($request->only(['name', 'default_from_email']));
$server->bounce_handler_id = $request->bounce_handler_id ?: null;
$server->feedback_loop_handler_id = $request->feedback_loop_handler_id ?: null;
if ($request->options) {
$server->setOptions($request->options);
}
$limitKey = $request->input('options.sending_limit');
if ($limitKey && $limitKey !== 'custom' && $limitKey !== 'current') {
$limits = SendingServer::sendingLimitValues()[$limitKey] ?? null;
if ($limits) {
$server->quota_value = $limits['quota_value'];
$server->quota_unit = $limits['quota_unit'];
$server->quota_base = $limits['quota_base'];
}
} elseif ($limitKey === 'custom') {
$server->quota_value = $request->quota_value;
$server->quota_unit = $request->quota_unit;
$server->quota_base = $request->quota_base;
}
$server->save();
return response()->json(['message' => trans('refactor/sending.servers.config_saved')]);
}
public function serverEnable(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$server = $customer->sendingServers()->where('uid', $uid)->firstOrFail();
if (!$customer->can('update', $server)) {
return $this->notAuthorized();
}
$server->enable();
return response()->json(['message' => trans('refactor/sending.servers.enabled')]);
}
public function serverDisable(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$server = $customer->sendingServers()->where('uid', $uid)->firstOrFail();
if (!$customer->can('update', $server)) {
return $this->notAuthorized();
}
$server->disable();
return response()->json(['message' => trans('refactor/sending.servers.disabled')]);
}
public function serverTest(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$server = $customer->sendingServers()->where('uid', $uid)->firstOrFail();
return view('refactor.pages.sending._server_test', [
'server' => $server,
]);
}
public function serverTestSend(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$server = $customer->sendingServers()->where('uid', $uid)->firstOrFail();
$validator = \Validator::make($request->all(), ['email' => 'required|email']);
if ($validator->fails()) {
return response()->json(['message' => $validator->errors()->first()], 422);
}
try {
$email = new \Symfony\Component\Mime\Email();
$msgId = \App\Library\StringHelper::generateMessageId(
\App\Library\StringHelper::getDomainFromEmail($request->user()->email)
);
$email->getHeaders()->addIdHeader('Message-ID', $msgId);
$email->getHeaders()->addTextHeader('X-Acelle-Message-Id', $msgId);
$email->subject('Test email from ' . config('app.name'));
$email->from($request->user()->email);
$email->to($request->email);
$email->text('This is a test email sent from your sending server to verify delivery is working.');
$server->sendTestEmail($email);
return response()->json(['message' => trans('refactor/sending.servers.test_sent')]);
} catch (\Exception $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
}
public function serverTestConnection(Request $request, $uid)
{
$customer = $request->user()->customer;
if (!EntitlementGate::allows($customer, EntitlementKey::HAS_OWN_SENDING_SERVER)) {
return $this->notAuthorized();
}
$server = $customer->sendingServers()->where('uid', $uid)->firstOrFail();
if (!$customer->can('update', $server)) {
return $this->notAuthorized();
}
// F4.1 (P11) — same service the `sending.test_connection` AI tool
// calls. Single source of truth for the probe on customer side.
$result = app(\App\Services\SendingServerHealthService::class)->testConnection($server);
if ($result['success']) {
return response()->json(['message' => trans('refactor/sending.servers.test_connection_ok')]);
}
return response()->json(['message' => $result['error_message'] ?? ''], 422);
}
// ─── Sender Verify Token (public, no auth) ──────────────────
public function senderVerify(Request $request, $token)
{
try {
$sender = \App\Model\Sender::verifyToken($token);
return view('refactor.pages.sending.sender_verified', compact('sender'));
} catch (\Exception $ex) {
return view('refactor.pages.sending.sender_failed', [
'message' => $ex->getMessage(),
]);
}
}
}