Files
budget/app/Mailgun.php
T
Ty Clifford 6686388834 - Implemented the complete first-run experience:
Account creation and guarded onboarding flow.
Current-cash starting balance with automatic rollover enabled.
Dynamic bill generation, shared/custom categories, recurring targets, 
and optional same-day onboarding transactions.
Optional dormant reserve and additional categories.
Mailgun email or authenticator-app 2FA configuration.
Responsive dark/light UI with no overflow at 390px.
Fixed administrator session-ID creation bug.
Core implementation: [OnboardingService.php (line 
59)](/Users/tyemeclifford/Documents/GH/budget/app/OnboardingService.php:59), 
[onboarding.php (line 
19)](/Users/tyemeclifford/Documents/GH/budget/views/onboarding.php:19), 
and [index.php (line 
143)](/Users/tyemeclifford/Documents/GH/budget/public/index.php:143).
Verification passed: financial self-test, onboarding integration test, 
full browser walkthrough, mobile layout, routing guards, and zero 
browser console errors. Live email delivery requires actual Mailgun 
credentials and was not sent during testing.
2026-06-15 14:51:44 -04:00

60 lines
2.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Budget;
use RuntimeException;
final class Mailgun
{
public function __construct(private Settings $settings)
{
}
public function configured(): bool
{
return $this->settings->get('mailgun.api_key', '') !== ''
&& $this->settings->get('mailgun.domain', '') !== ''
&& $this->settings->get('alerts.email', '') !== '';
}
public function send(string $recipient, string $subject, string $html): array
{
if (!$this->configured()) {
throw new RuntimeException('Mailgun settings are incomplete.');
}
$region = (string) $this->settings->get('mailgun.region', 'us');
$host = $region === 'eu' ? 'api.eu.mailgun.net' : 'api.mailgun.net';
$domain = (string) $this->settings->get('mailgun.domain', '');
$endpoint = sprintf('https://%s/v3/%s/messages', $host, rawurlencode($domain));
$body = http_build_query([
'from' => (string) $this->settings->get('mailgun.from', ''),
'to' => $recipient,
'subject' => $subject,
'html' => $html,
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => [
'Authorization: Basic ' . base64_encode('api:' . $this->settings->get('mailgun.api_key', '')),
'Content-Type: application/x-www-form-urlencoded',
'Content-Length: ' . strlen($body),
],
'content' => $body,
'ignore_errors' => true,
'timeout' => 15,
],
]);
$response = @file_get_contents($endpoint, false, $context);
$statusLine = $http_response_header[0] ?? 'HTTP/1.1 500 Unknown response';
preg_match('/\s(\d{3})\s/', $statusLine, $matches);
$status = (int) ($matches[1] ?? 500);
if ($response === false || $status >= 400) {
throw new RuntimeException('Mailgun rejected the message: ' . ($response ?: $statusLine));
}
return ['status' => $status, 'body' => $response];
}
}