Welche Mailwerte eine Kundeninstanz bekommt — und wann gar keine
Server aus app_settings, Zugangsdaten aus dem Postfach, an einer Stelle zusammengelegt. Fehlt eines von beiden, wird NICHTS geschrieben: eine halb eingetragene Mailkonfiguration laesst Nextcloud bei jedem Versand still scheitern. mailboxFor() ist die Naht, an der spaeter ein Konto je Kunde haengt.feat/versandtakt
parent
96bc2c5a07
commit
da9e71bc88
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Mail;
|
||||
|
||||
use App\Models\Instance;
|
||||
use App\Models\Mailbox;
|
||||
use App\Support\Settings;
|
||||
|
||||
/**
|
||||
* Welche Mailwerte die Nextcloud eines Kunden bekommt — und wann keine.
|
||||
*
|
||||
* Server und Zugangsdaten liegen in diesem Projekt an ZWEI Orten, und das ist
|
||||
* Absicht: der Server steht einmal in `app_settings` (ein Anbieterwechsel ist
|
||||
* dann eine Karte, nicht fuenf Zeilen), die Zugangsdaten je Absender am
|
||||
* Mailbox-Datensatz. Diese Klasse ist die einzige Stelle, die beides
|
||||
* zusammenlegt.
|
||||
*
|
||||
* `available() === false` heisst: NICHTS schreiben. Eine halb eingetragene
|
||||
* Mailkonfiguration ist schlimmer als keine — Nextcloud versucht dann bei
|
||||
* jedem Versand zu senden und scheitert still, ohne dass jemand es merkt.
|
||||
*
|
||||
* DIE NAHTSTELLE fuer spaeter: `mailboxFor()` gibt heute das gemeinsame
|
||||
* Versandkonto zurueck. Ein Konto je Kunde (ueber die mailcow-Schnittstelle
|
||||
* angelegt) haengt genau hier — der Bereitstellungsschritt und alles darueber
|
||||
* aendern sich dafuer nicht um eine Zeile.
|
||||
*/
|
||||
final class GuestMailConfig
|
||||
{
|
||||
/** Der Schluessel des gemeinsamen Versandkontos in `mailboxes`. */
|
||||
public const RELAY_KEY = 'instance-relay';
|
||||
|
||||
private function __construct(
|
||||
private readonly ?string $problem,
|
||||
/** @var array<string, string> */
|
||||
private readonly array $values,
|
||||
private readonly string $password,
|
||||
) {}
|
||||
|
||||
public static function for(Instance $instance): self
|
||||
{
|
||||
$host = trim((string) Settings::get('mail.host', ''));
|
||||
$port = (int) Settings::get('mail.port', 0);
|
||||
|
||||
if ($host === '' || $port < 1) {
|
||||
return new self('no_server', [], '');
|
||||
}
|
||||
|
||||
$box = self::mailboxFor($instance);
|
||||
|
||||
if ($box === null || ! $box->isConfigured()) {
|
||||
return new self('no_mailbox', [], '');
|
||||
}
|
||||
|
||||
// Die Adresse zerfaellt in die beiden Werte, die Nextcloud getrennt
|
||||
// fuehrt: den linken Teil als Absender, den rechten als Maildomain.
|
||||
[$local, $domain] = array_pad(explode('@', $box->address, 2), 2, '');
|
||||
|
||||
return new self(null, [
|
||||
'mail_smtpmode' => 'smtp',
|
||||
'mail_smtphost' => $host,
|
||||
'mail_smtpport' => (string) $port,
|
||||
'mail_smtpsecure' => (string) Settings::get('mail.encryption', 'tls'),
|
||||
'mail_smtpauth' => 'true',
|
||||
'mail_smtpname' => $box->smtpUsername(),
|
||||
'mail_from_address' => $local,
|
||||
'mail_domain' => $domain,
|
||||
], (string) $box->password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heute fuer jede Instanz dasselbe Konto. Siehe Klassenkopf — hier haengt
|
||||
* die spaetere Fassung mit einem Konto je Kunde.
|
||||
*/
|
||||
private static function mailboxFor(Instance $instance): ?Mailbox
|
||||
{
|
||||
return Mailbox::findByKey(self::RELAY_KEY);
|
||||
}
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
return $this->problem === null;
|
||||
}
|
||||
|
||||
/** `no_server`, `no_mailbox` oder null. */
|
||||
public function problem(): ?string
|
||||
{
|
||||
return $this->problem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alles ausser dem Passwort. Das geht einen eigenen Weg, damit es beim
|
||||
* Bauen der Befehle nicht versehentlich mitwandert.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function values(): array
|
||||
{
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
public function password(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<?php // tests/Feature/Mail/GuestMailConfigTest.php
|
||||
|
||||
use App\Models\Instance;
|
||||
use App\Models\Mailbox;
|
||||
use App\Services\Mail\GuestMailConfig;
|
||||
use App\Support\Settings;
|
||||
|
||||
function versandbereit(): void
|
||||
{
|
||||
Settings::set('mail.host', 'mail.clupilot.cloud');
|
||||
Settings::set('mail.port', 587);
|
||||
Settings::set('mail.encryption', 'tls');
|
||||
|
||||
Mailbox::factory()->create([
|
||||
'key' => 'instance-relay',
|
||||
'address' => 'noreply@clupilot.cloud',
|
||||
'username' => 'noreply@clupilot.cloud',
|
||||
'password' => 'geheim',
|
||||
'active' => true,
|
||||
'authenticates' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
it('baut die Werte aus Server UND Postfach zusammen', function () {
|
||||
versandbereit();
|
||||
// Instance kennt kein 'name'-Feld (siehe Migration) — GuestMailConfig
|
||||
// liest ohnehin nur das Postfach, nicht die Instanz selbst.
|
||||
$instance = Instance::factory()->create();
|
||||
|
||||
$config = GuestMailConfig::for($instance);
|
||||
|
||||
expect($config->available())->toBeTrue()
|
||||
->and($config->values())->toMatchArray([
|
||||
'mail_smtpmode' => 'smtp',
|
||||
'mail_smtphost' => 'mail.clupilot.cloud',
|
||||
'mail_smtpport' => '587',
|
||||
'mail_smtpsecure' => 'tls',
|
||||
'mail_smtpauth' => 'true',
|
||||
'mail_smtpname' => 'noreply@clupilot.cloud',
|
||||
'mail_from_address' => 'noreply',
|
||||
'mail_domain' => 'clupilot.cloud',
|
||||
])
|
||||
->and($config->password())->toBe('geheim');
|
||||
});
|
||||
|
||||
it('traegt das Passwort NICHT unter values()', function () {
|
||||
// values() wandert in Befehle. Das Passwort geht einen eigenen Weg, damit
|
||||
// niemand es versehentlich mit den uebrigen Werten mitschleift.
|
||||
versandbereit();
|
||||
|
||||
expect(GuestMailConfig::for(Instance::factory()->create())->values())
|
||||
->not->toHaveKey('mail_smtppassword');
|
||||
});
|
||||
|
||||
it('sagt ohne Mailserver, dass nichts geschrieben werden darf', function () {
|
||||
Mailbox::factory()->create(['key' => 'instance-relay', 'address' => 'noreply@clupilot.cloud', 'password' => 'geheim', 'active' => true]);
|
||||
Settings::set('mail.host', '');
|
||||
|
||||
$config = GuestMailConfig::for(Instance::factory()->create());
|
||||
|
||||
expect($config->available())->toBeFalse()
|
||||
->and($config->problem())->toBe('no_server');
|
||||
});
|
||||
|
||||
it('sagt ohne Absenderpostfach, dass nichts geschrieben werden darf', function () {
|
||||
Settings::set('mail.host', 'mail.clupilot.cloud');
|
||||
Settings::set('mail.port', 587);
|
||||
|
||||
$config = GuestMailConfig::for(Instance::factory()->create());
|
||||
|
||||
expect($config->available())->toBeFalse()
|
||||
->and($config->problem())->toBe('no_mailbox');
|
||||
});
|
||||
|
||||
it('weist ein Postfach ohne Zugangsdaten ab', function () {
|
||||
Settings::set('mail.host', 'mail.clupilot.cloud');
|
||||
Settings::set('mail.port', 587);
|
||||
Mailbox::factory()->create(['key' => 'instance-relay', 'address' => 'noreply@clupilot.cloud', 'password' => null, 'active' => true, 'authenticates' => true]);
|
||||
|
||||
expect(GuestMailConfig::for(Instance::factory()->create())->problem())->toBe('no_mailbox');
|
||||
});
|
||||
Loading…
Reference in New Issue