diff --git a/cron/flexiapi.cron b/cron/flexiapi.cron
new file mode 100644
index 0000000..a11ed25
--- /dev/null
+++ b/cron/flexiapi.cron
@@ -0,0 +1 @@
+* * * * * apache /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/artisan schedule:run >> /dev/null 2>&1
\ No newline at end of file
diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php
index 81bd739..4bfca3e 100644
--- a/flexiapi/app/Account.php
+++ b/flexiapi/app/Account.php
@@ -146,6 +146,11 @@ class Account extends Authenticatable
->latest();
}
+ public function uploadedVoicemails()
+ {
+ return $this->voicemails()->whereNotNull('name');
+ }
+
public function vcardsStorage()
{
return $this->hasMany(VcardStorage::class);
diff --git a/flexiapi/app/AccountFile.php b/flexiapi/app/AccountFile.php
index 404f28f..e8c81a7 100644
--- a/flexiapi/app/AccountFile.php
+++ b/flexiapi/app/AccountFile.php
@@ -12,19 +12,22 @@ class AccountFile extends Model
public const VOICEMAIL_CONTENTTYPES = ['audio/opus', 'audio/wav'];
public const FILES_PATH = 'files';
- protected $hidden = ['account_id', 'updated_at'];
+ protected $hidden = ['account_id', 'updated_at', 'sending_by_mail_at', 'sent_by_mail_at', 'sending_by_mail_tryouts'];
protected $appends = ['download_url'];
+ protected $casts = [
+ 'uploaded_at' => 'datetime',
+ ];
protected static function booted()
{
- static::deleting(function ($category) {
- Storage::delete($this->getPathAttribute());
+ static::deleting(function (AccountFile $accountFile) {
+ Storage::delete($accountFile->getPathAttribute());
});
}
public function account()
{
- return $this->belongsTo(Account::class);
+ return $this->belongsTo(Account::class)->withoutGlobalScopes();
}
public function getMaxUploadSizeAttribute(): ?int
@@ -42,11 +45,30 @@ class AccountFile extends Model
return self::FILES_PATH . '/' . $this->attributes['name'];
}
+ public function getUrlAttribute(): ?string
+ {
+ return !empty($this->attributes['name'])
+ && !empty($this->attributes['id'])
+ ? replaceHost(
+ route('file.show', ['uuid' => $this->attributes['id'], 'name' => $this->attributes['name']]),
+ $this->account->space->host
+ )
+ : null;
+ }
+
public function getDownloadUrlAttribute(): ?string
{
return !empty($this->attributes['name'])
&& !empty($this->attributes['id'])
- ? route('file.show', [$this->attributes['id'], $this->attributes['name']])
+ ? replaceHost(route(
+ 'file.download',
+ ['uuid' => $this->attributes['id'], 'name' => $this->attributes['name']]
+ ), $this->account->space->host)
: null;
}
+
+ public function isVoicemailAudio(): bool
+ {
+ return in_array($this->attributes['content_type'], self::VOICEMAIL_CONTENTTYPES);
+ }
}
diff --git a/flexiapi/app/Console/Commands/Accounts/SendVoicemailsEmails.php b/flexiapi/app/Console/Commands/Accounts/SendVoicemailsEmails.php
new file mode 100644
index 0000000..a5ba5ad
--- /dev/null
+++ b/flexiapi/app/Console/Commands/Accounts/SendVoicemailsEmails.php
@@ -0,0 +1,40 @@
+whereNull('sent_by_mail_at')
+ ->where('sending_by_mail_tryouts', '<', is_int($this->option('tryout'))
+ ? $this->option('tryout')
+ : 3)
+ ->get();
+
+ foreach ($voicemails as $voicemail) {
+ $voicemail->sending_by_mail_at = Carbon::now();
+ $voicemail->save();
+
+ if (Mail::to(users: $voicemail->account)->send(new Voicemail($voicemail))) {
+ $voicemail->sent_by_mail_at = Carbon::now();
+ $this->info('Voicemail sent to ' . $voicemail->account->identifier);
+ } else {
+ $voicemail->sending_by_mail_tryouts++;
+ $this->info('Error sending voicemail to ' . $voicemail->account->identifier);
+ }
+
+ $voicemail->save();
+ }
+ }
+}
diff --git a/flexiapi/app/Helpers/Utils.php b/flexiapi/app/Helpers/Utils.php
index 8762f65..351cbef 100644
--- a/flexiapi/app/Helpers/Utils.php
+++ b/flexiapi/app/Helpers/Utils.php
@@ -162,17 +162,12 @@ function resolveDomain(Request $request): string
: $request->space->domain;
}
-function maxUploadSize(): ?int
+function maxUploadSize(): int
{
- $uploadMaxSizeInBytes = ini_parse_quantity(ini_get('upload_max_filesize'));
- if ($uploadMaxSizeInBytes > 0) {
- return $uploadMaxSizeInBytes / 1024;
- }
-
- $postMaxSizeInBytes = ini_parse_quantity(ini_get('post_max_size'));
- if ($postMaxSizeInBytes > 0) {
- return $postMaxSizeInBytes / 1024;
- }
+ return min(
+ ini_parse_quantity(ini_get('upload_max_filesize')),
+ ini_parse_quantity(ini_get('post_max_size'))
+ );
}
function captchaConfigured(): bool
diff --git a/flexiapi/app/Http/Controllers/Account/FileController.php b/flexiapi/app/Http/Controllers/Account/FileController.php
new file mode 100644
index 0000000..a596887
--- /dev/null
+++ b/flexiapi/app/Http/Controllers/Account/FileController.php
@@ -0,0 +1,32 @@
+name != $name) {
+ abort(404);
+ }
+
+ return Storage::get($file->path);
+ }
+
+ public function download(string $uuid, string $name)
+ {
+ $file = AccountFile::findOrFail($uuid);
+
+ if ($file->name != $name) {
+ abort(404);
+ }
+
+ return Storage::download($file->path);
+ }
+}
diff --git a/flexiapi/app/Http/Controllers/Admin/Account/FileController.php b/flexiapi/app/Http/Controllers/Admin/Account/FileController.php
new file mode 100644
index 0000000..26d55eb
--- /dev/null
+++ b/flexiapi/app/Http/Controllers/Admin/Account/FileController.php
@@ -0,0 +1,32 @@
+files()->where('id', $fileId)->firstOrFail();
+
+ return view('admin.account.file.delete', [
+ 'account' => $account,
+ 'file' => $file
+ ]);
+ }
+
+ public function destroy(Request $request, int $accountId, string $fileId)
+ {
+ $account = Account::findOrFail($accountId);
+ $accountFile = $account->files()
+ ->where('id', $fileId)
+ ->firstOrFail();
+ $accountFile->delete();
+
+ return redirect()->route('admin.account.show', $account)->withFragment('#files');
+ }
+}
diff --git a/flexiapi/app/Http/Controllers/Api/Account/FileController.php b/flexiapi/app/Http/Controllers/Api/Account/FileController.php
index 6c61b9f..bcab52d 100644
--- a/flexiapi/app/Http/Controllers/Api/Account/FileController.php
+++ b/flexiapi/app/Http/Controllers/Api/Account/FileController.php
@@ -4,24 +4,13 @@ namespace App\Http\Controllers\Api\Account;
use App\AccountFile;
use App\Http\Controllers\Controller;
+use App\Rules\AudioMime;
use Carbon\Carbon;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class FileController extends Controller
{
- public function show(string $uuid, string $name)
- {
- $file = AccountFile::findOrFail($uuid);
-
- if ($file->name != $name) {
- abort(404);
- }
-
- return Storage::download($file->path);
- }
-
public function upload(Request $request, string $uuid)
{
$file = AccountFile::findOrFail($uuid);
@@ -30,14 +19,16 @@ class FileController extends Controller
abort(404);
}
- $request->validate([
- 'file' => 'required|file|mimetypes:' . $file->content_type
- ]);
+ $request->validate(['file' => 'required|file']);
+
+ if ($file->isVoicemailAudio()) {
+ $request->validate(['file' => [new AudioMime($file)]]);
+ }
$uploadedFile = $request->file('file');
$name = Str::random(8) . '_' . $uploadedFile->getClientOriginalName();
- if ($uploadedFile->storeAs('files', $name)) {
+ if ($uploadedFile->storeAs(AccountFile::FILES_PATH, $name)) {
$file->name = $name;
$file->size = $uploadedFile->getSize();
$file->uploaded_at = Carbon::now();
diff --git a/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php b/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php
index e2db717..42bf3b2 100644
--- a/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php
+++ b/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php
@@ -18,7 +18,6 @@ class VoicemailController extends Controller
public function store(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
-
$request->validate([
'sip_from' => 'nullable|starts_with:sip',
'content_type' => [
diff --git a/flexiapi/app/Mail/ExpiringSpace.php b/flexiapi/app/Mail/ExpiringSpace.php
index b540810..c315a54 100644
--- a/flexiapi/app/Mail/ExpiringSpace.php
+++ b/flexiapi/app/Mail/ExpiringSpace.php
@@ -5,7 +5,6 @@ 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;
diff --git a/flexiapi/app/Mail/NewsletterRegistration.php b/flexiapi/app/Mail/NewsletterRegistration.php
index bcd590d..b55c8d7 100644
--- a/flexiapi/app/Mail/NewsletterRegistration.php
+++ b/flexiapi/app/Mail/NewsletterRegistration.php
@@ -22,7 +22,6 @@ namespace App\Mail;
use App\Account;
use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
diff --git a/flexiapi/app/Mail/Provisioning.php b/flexiapi/app/Mail/Provisioning.php
index 097dd67..10995ef 100644
--- a/flexiapi/app/Mail/Provisioning.php
+++ b/flexiapi/app/Mail/Provisioning.php
@@ -22,7 +22,6 @@ namespace App\Mail;
use App\Account;
use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
diff --git a/flexiapi/app/Mail/RecoverByCode.php b/flexiapi/app/Mail/RecoverByCode.php
index 16f58ca..5aa0354 100644
--- a/flexiapi/app/Mail/RecoverByCode.php
+++ b/flexiapi/app/Mail/RecoverByCode.php
@@ -22,7 +22,6 @@ namespace App\Mail;
use App\Account;
use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
diff --git a/flexiapi/app/Mail/ResetPassword.php b/flexiapi/app/Mail/ResetPassword.php
index 4abc43a..e045bd7 100644
--- a/flexiapi/app/Mail/ResetPassword.php
+++ b/flexiapi/app/Mail/ResetPassword.php
@@ -20,10 +20,8 @@
namespace App\Mail;
use App\Account;
-use App\ResetPasswordEmailToken;
use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
diff --git a/flexiapi/app/Mail/Voicemail.php b/flexiapi/app/Mail/Voicemail.php
new file mode 100644
index 0000000..56e217c
--- /dev/null
+++ b/flexiapi/app/Mail/Voicemail.php
@@ -0,0 +1,44 @@
+accountFile->account->space->name .
+ ': ' .
+ __('New voice message from :sipfrom', ['sipfrom' => $this->accountFile->sip_from]),
+ );
+ }
+
+ public function content(): Content
+ {
+ return new Content(
+ markdown: 'mails.voicemail',
+ );
+ }
+
+ public function attachments(): array
+ {
+ return [
+ Attachment::fromStorage($this->accountFile->path)
+ ->withMime($this->accountFile->content_type)
+ ];
+ }
+}
diff --git a/flexiapi/app/Rules/AudioMime.php b/flexiapi/app/Rules/AudioMime.php
new file mode 100644
index 0000000..b1d10da
--- /dev/null
+++ b/flexiapi/app/Rules/AudioMime.php
@@ -0,0 +1,38 @@
+getMimeType()) {
+ case 'audio/opus':
+ $mimeType = 'audio/opus';
+ break;
+
+ case 'audio/vnd.wave':
+ case 'audio/wav':
+ case 'audio/wave':
+ case 'audio/x-wav':
+ case 'audio/x-pn-wav':
+ $mimeType = 'audio/wav';
+ break;
+ }
+
+ return $this->accountFile->content_type == $mimeType;
+ }
+
+ public function message()
+ {
+ return __('The file should have the declared mime-type');
+ }
+}
diff --git a/flexiapi/app/Rules/IsNotPhoneNumber.php b/flexiapi/app/Rules/IsNotPhoneNumber.php
index cd27155..c06e1c8 100644
--- a/flexiapi/app/Rules/IsNotPhoneNumber.php
+++ b/flexiapi/app/Rules/IsNotPhoneNumber.php
@@ -3,7 +3,6 @@
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
-use Respect\Validation\Validator;
use Propaganistas\LaravelPhone\PhoneNumber;
class IsNotPhoneNumber implements Rule
diff --git a/flexiapi/database/migrations/2025_10_20_093414_create_account_files_table.php b/flexiapi/database/migrations/2025_10_20_093414_create_account_files_table.php
index 6be05d2..cedde59 100644
--- a/flexiapi/database/migrations/2025_10_20_093414_create_account_files_table.php
+++ b/flexiapi/database/migrations/2025_10_20_093414_create_account_files_table.php
@@ -18,6 +18,9 @@ return new class extends Migration
$table->string('content_type')->index();
$table->text('sip_from')->nullable();
$table->dateTime('uploaded_at')->nullable();
+ $table->dateTime('sending_by_mail_at')->nullable();
+ $table->integer('sending_by_mail_tryouts')->default(0);
+ $table->dateTime('sent_by_mail_at')->nullable();
$table->timestamps();
});
}
diff --git a/flexiapi/lang/fr.json b/flexiapi/lang/fr.json
index 47a70d5..ab99cfd 100644
--- a/flexiapi/lang/fr.json
+++ b/flexiapi/lang/fr.json
@@ -101,6 +101,7 @@
"Features": "Fonctionnalités",
"Fill the related columns if you want to add an external account as well": "Remplissez également les colonnes suivantes si vous souhaitez ajouter un compte externe",
"Fill to change": "Remplir pour changer",
+ "Files": "Fichiers",
"From": "Depuis",
"Hello":"Bonjour",
"Host": "Hôte",
@@ -145,6 +146,7 @@
"New newsletter subscription": "Nouvelle inscription à votre newsletter",
"New Space": "Nouvel Espace",
"New user": "Nouvel utilisateur",
+ "New voice message from :sipfrom": "Nouveau message vocal de :sipfrom",
"Newsletter registration email address": "Addresse email d'inscription à la liste de diffusion",
"Next": "Suivant",
"No account yet?": "Pas encore de compte ?",
@@ -170,6 +172,7 @@
"QR Code scanning": "Scan de QR Code",
"Realm": "Royaume",
"Recover your account using your email": "Récupérer votre compte avec votre email",
+ "Recorded at": "Enregistré le",
"Register": "Inscription",
"Registrar": "Registrar",
"Registration confirmed": "Confirmation de l'inscription",
@@ -246,6 +249,7 @@
"Verify": "Vérifier",
"Via": "Via",
"Visit our user guide" : "Consulter notre guide utilisateur",
+ "Voicemails": "Messages vocaux",
"We are pleased to inform you that your account has been successfully created.":"Nous avons le plaisir de vous informer que votre compte a été créé avec succès.",
"We inform you that this space will expire on :date, in accordance with the expiration date defined in your subscription.":"Nous vous informons que l'espace expira le :date, conformément à la date d’expiration définie dans votre abonnement.",
"We received a request to recover your account on :space": "Nous avons reçu une demande de récupération de votre compte sur :space",
diff --git a/flexiapi/phpmd.xml b/flexiapi/phpmd.xml
index 953c29e..0b70f75 100644
--- a/flexiapi/phpmd.xml
+++ b/flexiapi/phpmd.xml
@@ -10,7 +10,6 @@
The Clean Code ruleset contains rules that enforce a clean code base. This includes rules from SOLID and object calisthenics.
-
| {{ __('Created') }} | ++ |
|---|---|
| {{ __('Empty') }} | +|
|
+ {{ $voicemail->created_at }}
+ @if ($voicemail->url)
+
+
+
+ @endif
+ @if ($voicemail->sip_from)
+ + {{ $voicemail->sip_from }} + @endif + |
+ + @if ($voicemail->url) + + + + + @endif + | +