Files
projectsend/app/Modules/Platform/Http/Controllers/EmailSettingsController.php
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00

185 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Platform\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Modules\Audit\Action;
use App\Modules\Audit\ActivityLogger;
use App\Modules\Platform\Capabilities\Capability;
use App\Modules\Platform\Capabilities\CapabilityRegistry;
use App\Modules\Platform\Notifications\TestEmailNotification;
use App\Modules\Platform\Settings\MailConfigApplier;
use App\Modules\Platform\Settings\MailProvider;
use App\Modules\Platform\Settings\MailProviderSettings;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
use Throwable;
/**
* The master switch for transactional email, the SMTP transport itself
* (a generic form with provider presets — see MailProvider), and the
* admin notification recipient list.
*
* Transport (provider/host/port/username/password/encryption) is
* community-edition only (Capability::EmailTransportConfigure) — cloud
* operates its own relay. Sender identity (from_address/from_name) and
* the notification toggle/recipients stay editable in both editions.
*/
class EmailSettingsController extends Controller
{
public function __construct(
private readonly Settings $settings,
private readonly ActivityLogger $activity,
private readonly MailConfigApplier $mailConfig,
private readonly CapabilityRegistry $capabilities,
) {}
public function edit(Request $request): Response
{
$adminEmails = $this->settings->get(Setting::AdminNotificationEmails);
$mailProvider = MailProviderSettings::current();
return Inertia::render('system/settings/email', [
'email_notifications_enabled' => $this->settings->get(Setting::EmailNotificationsEnabled),
'admin_notification_emails' => is_array($adminEmails) ? $adminEmails : [],
'mail_provider' => [
'provider' => $mailProvider->provider->value,
'host' => $mailProvider->host ?? '',
'port' => $mailProvider->port,
'username' => $mailProvider->username ?? '',
'has_password' => $mailProvider->password !== null && $mailProvider->password !== '',
'encryption' => $mailProvider->encryption,
'from_address' => $mailProvider->from_address ?? '',
'from_name' => $mailProvider->from_name ?? '',
],
'mail_provider_presets' => array_map(fn (MailProvider $provider): array => [
'value' => $provider->value,
'label' => $provider->label(),
'host' => $provider->defaultHost(),
'port' => $provider->defaultPort(),
], MailProvider::cases()),
'test_result' => $request->session()->get('mail_test_result'),
]);
}
/**
* One form, one save: the notification toggle, admin recipients, and
* SMTP transport all persist together.
*/
public function update(Request $request): RedirectResponse
{
$canConfigureTransport = $this->capabilities->has(Capability::EmailTransportConfigure);
$rules = [
'email_notifications_enabled' => ['required', 'boolean'],
'admin_notification_emails' => ['required', 'array', 'min:1'],
'admin_notification_emails.*' => ['email', 'max:255'],
'from_address' => ['required', 'email', 'max:255'],
'from_name' => ['required', 'string', 'max:255'],
];
if ($canConfigureTransport) {
$rules += [
'provider' => ['required', Rule::in(array_map(fn (MailProvider $p): string => $p->value, MailProvider::cases()))],
'host' => ['required', 'string', 'max:255'],
'port' => ['required', 'integer', 'between:1,65535'],
'username' => ['nullable', 'string', 'max:255'],
'password' => ['nullable', 'string', 'max:255'],
'encryption' => ['required', Rule::in(['none', 'tls', 'ssl'])],
];
}
$validated = $request->validate($rules);
$adminEmails = array_values(array_unique($validated['admin_notification_emails']));
$this->settings->set(Setting::EmailNotificationsEnabled, $validated['email_notifications_enabled']);
$this->settings->set(Setting::AdminNotificationEmails, $adminEmails);
$mailProvider = MailProviderSettings::current();
// Transport fields are simply never read from the request when the
// capability is absent — a hand-crafted PATCH can't smuggle a
// custom relay into a cloud install through this endpoint either.
if ($canConfigureTransport) {
$mailProvider->fill([
'provider' => $validated['provider'],
'host' => $validated['host'],
'port' => $validated['port'],
'username' => $validated['username'] ?? null,
'encryption' => $validated['encryption'],
]);
// A blank password keeps whatever is already stored — the field
// is never round-tripped to the browser (only `has_password` is).
if (is_string($validated['password'] ?? null) && $validated['password'] !== '') {
$mailProvider->password = $validated['password'];
}
}
$mailProvider->from_address = $validated['from_address'];
$mailProvider->from_name = $validated['from_name'];
$mailProvider->save();
$this->mailConfig->flush();
$this->mailConfig->apply();
// The long-running queue worker cached the old config at boot;
// this signals it to restart so queued/future mail uses the new
// settings without a manual container restart.
Artisan::call('queue:restart');
$this->activity->log(Action::SettingsUpdated, context: ['section' => 'email']);
return back()->with('success', __('Email settings saved.'));
}
/**
* Sends immediately (bypassing the queue, unlike every other
* notification in this app) to whatever address the admin enters,
* regardless of the toggle above — this exists specifically to
* verify SMTP works before switching it on, and needs a real,
* synchronous result to show, not a "queued" success message.
*/
public function sendTest(Request $request): RedirectResponse
{
abort_unless($this->capabilities->has(Capability::EmailTransportConfigure), 404);
$validated = $request->validate([
'recipient' => ['required', 'email', 'max:255'],
]);
$host = config('mail.mailers.smtp.host');
$port = config('mail.mailers.smtp.port');
$hostPort = (is_string($host) ? $host : '').':'.(is_scalar($port) ? (string) $port : '');
// Which of the two this is has to travel with the message rather
// than be inferred from its text: the frontend colours the result,
// and sniffing for the word "Success" would stop working the
// moment somebody reads this screen in another language.
try {
Notification::route('mail', $validated['recipient'])->notifyNow(new TestEmailNotification);
$result = [
'ok' => true,
'message' => __('Success: test email sent to :email via :hostport.', [
'email' => $validated['recipient'],
'hostport' => $hostPort,
]),
];
} catch (Throwable $e) {
$result = ['ok' => false, 'message' => __('Failed to send: :error', ['error' => $e->getMessage()])];
}
return back()->with('mail_test_result', $result);
}
}