Fix FLEXIAPI-224 Add a console script to send Space Expiration emails

This commit is contained in:
Timothée Jaussoin 2025-05-13 08:46:46 +00:00
parent 48961ea194
commit b0b6ab2c51
13 changed files with 129 additions and 14 deletions

View file

@ -35,6 +35,7 @@ v1.7
- Fix FLEXIAPI-287 Refactor the emails templates
- Fix FLEXIAPI-286 Send an account_recovery_token using a push notification and protect the account recovery using phone page with the account_recovery_token
- Fix FLEXIAPI-293 Remove the (long) outdated general documentation
- Fix FLEXIAPI-224 Add a console script to send Space Expiration emails
v1.6
----

View file

@ -61,9 +61,9 @@ php artisan spaces:create-update beta.sip beta.myhost.com "Beta Space"
5. Configure your Spaces.
6. Remove the instance based environnement variables (see **Changed** above) and configure them directly in the spaces using the API or Web Panel.
6. (Optional) Import the old instance DotEnv environnement variables into a space.
7. (Optional) Import the old instance DotEnv environnement variables into a space.
7. Remove the instance based environnement variables (see **Changed** above) and configure them directly in the spaces using the API or Web Panel.
⚠️ Be careful, during this import only the project DotEnv file variables will be imported, other environnement (eg. set in Apache, nginx or Docker) will be ignored.

View file

@ -5,3 +5,4 @@ sudo -su www-data && php artisan digest:clear-nonces 60
sudo -su www-data && php artisan accounts:clear-api-keys 60
sudo -su www-data && php artisan accounts:clear-accounts-tombstones 7 --apply
sudo -su www-data && php artisan accounts:clear-unconfirmed 30 --apply
sudo -su www-data && php artisan spaces:expiration-emails

View file

@ -5,3 +5,4 @@ php artisan digest:clear-nonces 60
php artisan accounts:clear-api-keys 60
php artisan accounts:clear-accounts-tombstones 7 --apply
php artisan accounts:clear-unconfirmed 30 --apply
php artisan spaces:expiration-emails

View file

@ -80,7 +80,6 @@ class CreateAdminAccount extends Command
$account = new Account;
$account->username = $username;
$account->domain = $domain;
$account->email = 'admin_test@sip.example.org';
$account->activated = true;
$account->user_agent = 'Test';
$account->ip_address = '0.0.0.0';

View file

@ -0,0 +1,57 @@
<?php
namespace App\Console\Commands\Spaces;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use Carbon\Carbon;
use App\Mail\ExpiringSpace;
use App\Space;
class ExpirationEmails extends Command
{
protected $signature = 'spaces:expiration-emails {days?}';
protected $description = 'Send an expiration email on the designated configured days before expiration. Days must be ordered descending and comma separated (eg. 7,3,1)';
public function handle()
{
$days = ['7','3','1'];
if ($this->argument('days')) {
preg_match_all('/\d++/', $this->argument('days'), $matches);
if (!empty($matches[0])) {
$i = 0;
while ($i + 1 < count($matches[0]) && (int)$matches[0][$i] > (int)$matches[0][$i + 1]) {
$i++;
}
if ($i != count($matches[0]) - 1) {
$this->error('The days must be integer, ordered descending and comma separated');
return Command::FAILURE;
}
$days = $matches[0];
}
}
$expiringSpaces = Space::whereNotNull('expire_at')->whereDate('expire_at', '>=', Carbon::now())->get();
foreach ($expiringSpaces as $expiringSpace) {
if (in_array($expiringSpace->daysLeft, $days)) {
$this->info($expiringSpace->name . ' (' . $expiringSpace->host . ') is expiring in ' . $expiringSpace->daysLeft . ' days');
$admins = $expiringSpace->admins()->withoutGlobalScopes()->whereNotNull('email')->get();
$this->info('Sending an email to the admins ' . $admins->implode('email', ','));
foreach ($admins as $admin) {
Mail::to($admin->email)->send(new ExpiringSpace($expiringSpace));
}
}
}
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Mail;
use App\Space;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ExpiringSpace extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Space $space
) {
}
public function envelope(): Envelope
{
return new Envelope(
subject: $this->space->name . ': '. __('Space is expiring in :days days', ['days' => $this->space->daysLeft]),
);
}
public function content(): Content
{
return new Content(
markdown: 'mails.expiring_space',
);
}
}

View file

@ -66,7 +66,7 @@ class Space extends Model
];
public const HOST_REGEX = '[\w\-]+';
public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)';
public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-z0-9-]{1,63}(?<!-)\.)+[a-z]{2,63}$)';
public function accounts()
{
@ -129,7 +129,7 @@ class Space extends Model
public function getDaysLeftAttribute(): ?int
{
if ($this->expire_at != null) {
return (int)$this->expire_at->diffInDays(Carbon::now());
return (int)$this->expire_at->diffInDays(Carbon::now()) + 1;
}
return null;

View file

@ -167,6 +167,7 @@
"Sip Adress": "Adresse SIP",
"SIP Domain": "Domaine SIP",
"Space": "Espace",
"Space is expiring in :days days": "Votre Espace expire dans %d jours",
"Spaces": "Espaces",
"Statistics": "Statistiques",
"Subdomain": "Sous-domaine",

View file

@ -74,7 +74,7 @@
</div>
@endif
@if ($account->accountRecoveryTokens)
@if ($account->accountRecoveryTokens->isNotEmpty())
<div class="card large">
<h3>Account Recovery Tokens</h3>
<table>

View file

@ -40,6 +40,11 @@
@else
<i class="ph">infinity</i>
@endif
@if ($space->expire_at)
<br />
<small>{{ $space->expire_at->format('d-m-Y') }}</small>
@endif
</td>
</tr>
@endforeach

View file

@ -0,0 +1,14 @@
@extends('mails.layout')
@section('content')
# {{ $space->name }} is expiring in {{ $space->daysLeft }} days
You are one of the administrator of the {{ $space->name }} space configured on our service.
We inform you that this Space is officialy expiring on **{{ $space->expire_at->format('d-m-Y') }}**.
After that day you and your registered users will not be able to use the features provided by your subscription anymore.
Be sure to renew your subscription if you would like to continue to use our services.
@endsection

View file

@ -203,6 +203,12 @@ Route::middleware(['web_panel_enabled', 'space.check'])->group(function () {
});
Route::name('account.')->prefix('accounts')->group(function () {
Route::name('import.')->prefix('import')->controller(AccountImportController::class)->group(function () {
Route::get('/', 'create')->name('create');
Route::post('/', 'store')->name('store');
Route::post('handle', 'handle')->name('handle');
});
Route::middleware(['intercom_features'])->group(function () {
Route::name('type.')->prefix('types')->controller(AccountTypeController::class)->group(function () {
Route::get('/', 'index')->name('index');
@ -260,12 +266,6 @@ Route::middleware(['web_panel_enabled', 'space.check'])->group(function () {
Route::get('send', 'send')->name('send');
});
Route::name('import.')->prefix('import')->controller(AccountImportController::class)->group(function () {
Route::get('/', 'create')->name('create');
Route::post('/', 'store')->name('store');
Route::post('handle', 'handle')->name('handle');
});
Route::name('contact.')->prefix('{account}/contacts')->controller(AccountContactController::class)->group(function () {
Route::get('/', 'index')->name('index');
Route::get('create', 'create')->name('create');