Allow rules to forward to multiple recipients

This commit is contained in:
Will Browning 2025-10-16 20:34:04 +01:00
parent 0528b73e4f
commit 41699c6dfa
23 changed files with 689 additions and 500 deletions

View File

@ -14,6 +14,7 @@ use App\Notifications\DisallowedReplySendAttempt;
use App\Notifications\FailedDeliveryNotification;
use App\Notifications\NearBandwidthLimit;
use App\Notifications\SpamReplySendAttempt;
use App\Services\UserRuleChecker;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
@ -226,8 +227,7 @@ class ReceiveEmail extends Command
exit(0);
} else {
// Check if the spam header is present from Rspamd
$this->handleForward($aliasable ?? null, $this->parser->getHeader('X-AnonAddy-Spam') === 'Yes');
$this->handleForward($aliasable ?? null);
}
}
} catch (\Throwable $e) {
@ -252,7 +252,21 @@ class ReceiveEmail extends Command
{
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'R');
$message = new ReplyToEmail($this->user, $this->alias, $emailData);
// Check user rules and get rule IDs that have satisfied conditions
$ruleIdsAndActions = UserRuleChecker::getRuleIdsAndActionsForReplies($this->user, $emailData, $this->alias);
$ruleIds = null;
if (! empty($ruleIdsAndActions)) {
if (UserRuleChecker::shouldBlockEmail($ruleIdsAndActions)) {
$this->alias->increment('emails_blocked', 1, ['last_blocked' => now()]);
exit(0);
}
$ruleIds = array_keys($ruleIdsAndActions);
}
$message = new ReplyToEmail($this->user, $this->alias, $emailData, $ruleIds);
Mail::to($destination)->queue($message);
}
@ -271,16 +285,36 @@ class ReceiveEmail extends Command
// Hydrate all alias fields
$this->alias->refresh();
$isNewAlias = true;
}
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'S');
$message = new SendFromEmail($this->user, $this->alias, $emailData);
// Check user rules and get rule IDs that have satisfied conditions
$ruleIdsAndActions = UserRuleChecker::getRuleIdsAndActionsForSends($this->user, $emailData, $this->alias);
$ruleIds = null;
if (! empty($ruleIdsAndActions)) {
if (UserRuleChecker::shouldBlockEmail($ruleIdsAndActions)) {
// If it is a new alias that has been created on the fly, delete it.
if ($isNewAlias ?? false) {
$this->alias->forceDelete();
} else {
$this->alias->increment('emails_blocked', 1, ['last_blocked' => now()]);
}
exit(0);
}
$ruleIds = array_keys($ruleIdsAndActions);
}
$message = new SendFromEmail($this->user, $this->alias, $emailData, $ruleIds);
Mail::to($destination)->queue($message);
}
protected function handleForward($aliasable, $isSpam)
protected function handleForward($aliasable)
{
if (is_null($this->alias)) {
// This is a new alias
@ -325,12 +359,45 @@ class ReceiveEmail extends Command
if (isset($recipientIds)) {
$this->alias->recipients()->sync($recipientIds);
}
$isNewAlias = true;
}
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size);
$this->alias->verifiedRecipientsOrDefault()->each(function ($aliasRecipient) use ($emailData, $isSpam) {
$message = (new ForwardEmail($this->alias, $emailData, $aliasRecipient, $isSpam));
// Check user rules and get rule IDs that have satisfied conditions
$ruleIdsAndActions = UserRuleChecker::getRuleIdsAndActionsForForwards($this->user, $emailData, $this->alias);
$ruleIds = null;
$recipientsToForwardTo = $this->alias->verifiedRecipientsOrDefault();
if (! empty($ruleIdsAndActions)) {
if (UserRuleChecker::shouldBlockEmail($ruleIdsAndActions)) {
// If it is a new alias that has been created on the fly, delete it.
if ($isNewAlias ?? false) {
$this->alias->forceDelete();
} else {
$this->alias->increment('emails_blocked', 1, ['last_blocked' => now()]);
}
exit(0);
}
$ruleIds = array_keys($ruleIdsAndActions);
$forwardToRecipientIds = UserRuleChecker::getRecipientIdsToForwardToFromRuleIdsAndActions($ruleIdsAndActions);
if (! empty($forwardToRecipientIds)) {
$recipients = $this->user->verifiedRecipients()->whereIn('id', $forwardToRecipientIds)->get();
if ($recipients) {
$recipientsToForwardTo = $this->user->verifiedRecipients()->whereIn('id', $forwardToRecipientIds)->get();
}
}
}
$recipientsToForwardTo->each(function ($aliasRecipient) use ($emailData, $ruleIds) {
$message = (new ForwardEmail($this->alias, $emailData, $aliasRecipient, $ruleIds));
Mail::to($aliasRecipient->email)->queue($message);
});

View File

@ -63,6 +63,6 @@ class ForgotUsernameController extends Controller
]);
}
$request->validate(['email' => 'required|ascii|max:254|email:rfc']);
$request->validate(['email' => 'required|string|ascii|max:254|email:rfc']);
}
}

View File

@ -87,7 +87,7 @@ class RegisterController extends Controller
'required',
'string',
'ascii',
'email:rfc,dns',
App::environment(['local', 'testing']) ? 'email:rfc' : 'email:rfc,dns',
'max:254',
'confirmed',
new RegisterUniqueRecipient,

View File

@ -2,8 +2,10 @@
namespace App\Http\Requests;
use App\Rules\NotLocalRecipient;
use App\Rules\RegisterUniqueRecipient;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\App;
class EditDefaultRecipientRequest extends FormRequest
{
@ -28,9 +30,12 @@ class EditDefaultRecipientRequest extends FormRequest
'email' => [
'bail',
'required',
'email:rfc,dns',
'string',
'ascii',
App::environment(['local', 'testing']) ? 'email:rfc' : 'email:rfc,dns',
'max:254',
new RegisterUniqueRecipient,
new NotLocalRecipient,
'not_in:'.$this->user()->email,
],
'current' => 'required|string|current_password',

View File

@ -5,6 +5,7 @@ namespace App\Http\Requests;
use App\Rules\NotLocalRecipient;
use App\Rules\UniqueRecipient;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\App;
class StoreRecipientRequest extends FormRequest
{
@ -31,7 +32,7 @@ class StoreRecipientRequest extends FormRequest
'required',
'string',
'ascii',
'email:rfc,dns',
App::environment(['local', 'testing']) ? 'email:rfc' : 'email:rfc,dns',
'max:254',
new UniqueRecipient,
new NotLocalRecipient,

View File

@ -79,23 +79,34 @@ class StoreRuleRequest extends FormRequest
'array',
'max:5',
],
'actions.*.type' => [
'required',
'distinct',
Rule::in([
'subject',
'displayFrom',
'encryption',
'banner',
'block',
'removeAttachments',
'forwardTo',
// 'webhook',
]),
],
'actions.*.type' => Rule::forEach(function ($value, $attribute, $data, $action) {
$rules = [
'required',
Rule::in([
'subject',
'displayFrom',
'encryption',
'banner',
'block',
'removeAttachments',
'forwardTo',
// 'webhook',
]),
];
// If the action type is not forwardTo then do not allow duplicates
if ($action['type'] !== 'forwardTo') {
$rules[] = 'distinct';
}
return $rules;
}),
'actions.*.value' => Rule::forEach(function ($value, $attribute, $data, $action) {
if ($action['type'] === 'forwardTo') {
return [Rule::in(user()->verifiedRecipients()->pluck('id')->toArray())]; // Must be a valid verified recipient
return [
Rule::in(user()->verifiedRecipients()->pluck('id')->toArray()),
'distinct',
]; // Must be a valid verified recipient
}
return [

View File

@ -8,7 +8,7 @@ use App\Models\Alias;
use App\Models\EmailData;
use App\Models\Recipient;
use App\Notifications\FailedDeliveryNotification;
use App\Traits\CheckUserRules;
use App\Traits\ApplyUserRules;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -21,7 +21,7 @@ use Throwable;
class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
{
use CheckUserRules;
use ApplyUserRules;
use Queueable;
use SerializesModels;
@ -65,6 +65,8 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $isSpam;
protected $failedDmarc;
protected $resend;
protected $resendFromEmail;
@ -105,12 +107,14 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $verpDomain;
protected $ruleIds;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(Alias $alias, EmailData $emailData, Recipient $recipient, $isSpam = false, $resend = false)
public function __construct(Alias $alias, EmailData $emailData, Recipient $recipient, $resend = false, $ruleIds = null)
{
$this->user = $alias->user;
$this->alias = $alias;
@ -209,7 +213,9 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->fingerprint = $recipient->should_encrypt && ! $this->isAlreadyEncrypted() ? $recipient->fingerprint : null;
$this->bannerLocationText = $this->bannerLocationHtml = $this->isAlreadyEncrypted() || $resend ? 'off' : $this->alias->user->banner_location;
$this->isSpam = $isSpam;
$this->ruleIds = $ruleIds;
$this->isSpam = $emailData->isSpam;
$this->failedDmarc = $emailData->failedDmarc;
}
/**
@ -402,12 +408,15 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->replacedSubject = $this->user->email_subject ? ' with subject "'.base64_decode($this->emailSubject).'"' : null;
$this->checkRules('Forwards');
if ($this->ruleIds) {
$this->applyRulesByIds($this->ruleIds);
}
$this->email->with([
'locationText' => $this->bannerLocationText,
'locationHtml' => $this->bannerLocationHtml,
'isSpam' => $this->isSpam,
'failedDmarc' => $this->failedDmarc,
'deactivateUrl' => $this->deactivateUrl,
'aliasEmail' => $this->alias->email,
'aliasDomain' => $this->alias->domain,

View File

@ -7,7 +7,7 @@ use App\Models\Alias;
use App\Models\EmailData;
use App\Models\User;
use App\Notifications\FailedDeliveryNotification;
use App\Traits\CheckUserRules;
use App\Traits\ApplyUserRules;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -19,7 +19,7 @@ use Throwable;
class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
{
use CheckUserRules;
use ApplyUserRules;
use Queueable;
use SerializesModels;
@ -59,12 +59,14 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $verpDomain;
protected $ruleIds;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, Alias $alias, EmailData $emailData)
public function __construct(User $user, Alias $alias, EmailData $emailData, $ruleIds = null)
{
$this->user = $user;
$this->alias = $alias;
@ -125,6 +127,7 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->size = $emailData->size;
$this->inReplyTo = $emailData->inReplyTo;
$this->references = $emailData->references;
$this->ruleIds = $ruleIds;
}
/**
@ -213,7 +216,9 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
]);
}
$this->checkRules('Replies');
if ($this->ruleIds) {
$this->applyRulesByIds($this->ruleIds);
}
$this->email->with([
'userId' => $this->user->id,

View File

@ -7,7 +7,7 @@ use App\Models\Alias;
use App\Models\EmailData;
use App\Models\User;
use App\Notifications\FailedDeliveryNotification;
use App\Traits\CheckUserRules;
use App\Traits\ApplyUserRules;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -19,7 +19,7 @@ use Throwable;
class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
{
use CheckUserRules;
use ApplyUserRules;
use Queueable;
use SerializesModels;
@ -55,12 +55,14 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $verpDomain;
protected $ruleIds;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, Alias $alias, EmailData $emailData)
public function __construct(User $user, Alias $alias, EmailData $emailData, $ruleIds = null)
{
$this->user = $user;
$this->alias = $alias;
@ -119,6 +121,7 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->encryptedParts = $emailData->encryptedParts ?? null;
$this->displayFrom = $alias->getFromName();
$this->size = $emailData->size;
$this->ruleIds = $ruleIds;
}
/**
@ -197,7 +200,9 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
]);
}
$this->checkRules('Sends');
if ($this->ruleIds) {
$this->applyRulesByIds($this->ruleIds);
}
$this->email->with([
'userId' => $this->user->id,

View File

@ -58,6 +58,10 @@ class EmailData
public $receivedHeaders;
public $failedDmarc;
public $isSpam;
public $encryptedParts;
public $isInlineEncrypted;
@ -144,6 +148,9 @@ class EmailData
$this->authenticationResults = $parser->getHeader('X-AnonAddy-Authentication-Results');
$this->receivedHeaders = $parser->getRawHeader('Received');
$this->isSpam = $parser->getHeader('X-AnonAddy-Spam') === 'Yes';
$this->failedDmarc = Str::contains($this->authenticationResults, 'dmarc=fail');
$isReplyOrSend = in_array($emailType, ['R', 'S']);
if ($parser->getParts()[1]['content-type'] === 'multipart/encrypted') {

View File

@ -183,16 +183,14 @@ class FailedDelivery extends Model
$emailData = new EmailData($parser, $this->sender, strlen($email), 'F', true);
$isSpam = $parser->getHeader('X-AnonAddy-Spam') === 'Yes';
if ($verifiedRecipientIds) {
$recipients = $this->user->verifiedRecipients()->find($verifiedRecipientIds);
} else {
$recipients = $this->alias->verifiedRecipientsOrDefault();
}
$recipients->each(function ($aliasRecipient) use ($emailData, $isSpam) {
$message = new ForwardEmail($this->alias, $emailData, $aliasRecipient, $isSpam, true);
$recipients->each(function ($aliasRecipient) use ($emailData) {
$message = new ForwardEmail($this->alias, $emailData, $aliasRecipient, true);
Mail::to($aliasRecipient->email)->queue($message);
});

View File

@ -0,0 +1,195 @@
<?php
namespace App\Services;
use App\Models\Alias;
use App\Models\EmailData;
use App\Models\User;
use Illuminate\Support\Str;
class UserRuleChecker
{
protected $user;
protected $emailData;
protected $alias;
protected $sender;
protected $subject;
public function __construct(User $user, EmailData $emailData, Alias $alias)
{
$this->user = $user;
$this->emailData = $emailData;
$this->alias = $alias;
$this->sender = $emailData->sender;
$this->subject = $emailData->subject;
}
/**
* Get rule IDs that have satisfied conditions for a specific email type
*/
protected function getRuleIdsAndActions(string $emailType): array
{
$ruleIdsAndActions = [];
$method = "activeRulesFor{$emailType}Ordered";
$rules = $this->user->{$method};
foreach ($rules as $rule) {
// Check if the conditions of the rule are satisfied
if ($this->ruleConditionsSatisfied($rule->conditions, $rule->operator)) {
$ruleIdsAndActions[$rule->id] = $rule->actions;
// Increment applied count
$rule->increment('applied', 1, ['last_applied' => now()]);
}
}
return $ruleIdsAndActions;
}
/**
* Check if rule conditions are satisfied
*/
protected function ruleConditionsSatisfied(array $conditions, string $logicalOperator): bool
{
$results = collect();
foreach ($conditions as $condition) {
$results->push($this->lookupConditionType($condition));
}
$result = $results->unique();
if ($logicalOperator === 'OR') {
return $result->contains(true);
}
// Logical operator is AND so return false if any conditions are not met
return ! $result->contains(false);
}
/**
* Look up condition type and check if it's satisfied
*/
protected function lookupConditionType(array $condition): bool
{
switch ($condition['type']) {
case 'sender':
return $this->conditionSatisfied($this->emailData->sender, $condition);
case 'subject':
return $this->conditionSatisfied(base64_decode($this->emailData->subject), $condition); // Remember to base64_decode any encoded properties of emailData
case 'alias':
return $this->conditionSatisfied($this->alias->email, $condition);
case 'alias_description':
return $this->conditionSatisfied($this->alias->description, $condition);
default:
return false;
}
}
/**
* Check if a specific condition is satisfied
*/
protected function conditionSatisfied(string $variable, array $condition): bool
{
$values = collect($condition['values']);
switch ($condition['match']) {
case 'is exactly':
return $values->contains(function ($value) use ($variable) {
return $variable === $value;
});
case 'is not':
return ! $values->contains(function ($value) use ($variable) {
return $variable === $value;
});
case 'contains':
return $values->contains(function ($value) use ($variable) {
return Str::contains($variable, $value);
});
case 'does not contain':
return ! $values->contains(function ($value) use ($variable) {
return Str::contains($variable, $value);
});
case 'starts with':
return $values->contains(function ($value) use ($variable) {
return Str::startsWith($variable, $value);
});
case 'does not start with':
return ! $values->contains(function ($value) use ($variable) {
return Str::startsWith($variable, $value);
});
case 'ends with':
return $values->contains(function ($value) use ($variable) {
return Str::endsWith($variable, $value);
});
case 'does not end with':
return ! $values->contains(function ($value) use ($variable) {
return Str::endsWith($variable, $value);
});
case 'matches regex':
return $values->contains(function ($value) use ($variable) {
return Str::isMatch("/{$value}/", $variable);
});
case 'does not match regex':
return ! $values->contains(function ($value) use ($variable) {
return Str::isMatch("/{$value}/", $variable);
});
default:
return false;
}
}
/**
* Static method to get rule IDs for forwards (convenience method)
*/
public static function getRuleIdsAndActionsForForwards(User $user, EmailData $emailData, Alias $alias): array
{
$checker = new self($user, $emailData, $alias);
return $checker->getRuleIdsAndActions('Forwards');
}
/**
* Static method to get rule IDs for replies (convenience method)
*/
public static function getRuleIdsAndActionsForReplies(User $user, EmailData $emailData, Alias $alias): array
{
$checker = new self($user, $emailData, $alias);
return $checker->getRuleIdsAndActions('Replies');
}
/**
* Static method to get rule IDs for sends (convenience method)
*/
public static function getRuleIdsAndActionsForSends(User $user, EmailData $emailData, Alias $alias): array
{
$checker = new self($user, $emailData, $alias);
return $checker->getRuleIdsAndActions('Sends');
}
public static function getRecipientIdsToForwardToFromRuleIdsAndActions($ruleIdsAndActions): array
{
// Limit to a total of 10 forwardTo recipients.
return collect($ruleIdsAndActions)
->flatten(1)
->where('type', 'forwardTo')
->pluck('value')
->unique()
->take(10)
->all();
}
public static function shouldBlockEmail($ruleIdsAndActions): bool
{
return collect($ruleIdsAndActions)
->flatten(1)
->contains('type', 'block');
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Traits;
trait ApplyUserRules
{
protected $emailType;
public function applyRulesByIds(array $ruleIds)
{
if (empty($ruleIds)) {
return;
}
$this->user->rules()->whereIn('id', $ruleIds)->each(function ($rule) {
// Apply actions for that rule
collect($rule->actions)->each(function ($action) {
$this->applyAction($action);
});
});
}
protected function applyAction($action)
{
switch ($action['type']) {
case 'subject':
$this->replacedSubject = ' with subject "'.base64_decode($this->emailSubject).'"';
$this->email->subject = $action['value'];
break;
case 'displayFrom':
$this->email->from = [];
$this->displayFrom = $action['value'];
$this->email->from($this->fromEmail, $action['value']);
break;
case 'encryption':
if ($action['value'] == false) {
// detach the openpgpsigner from the email...
if (isset($this->fingerprint)) {
$this->fingerprint = null;
}
}
break;
case 'banner':
if (in_array($action['value'], ['top', 'bottom', 'off'])) {
if ($this->emailHtml) {
// Turn off the banner for the plain text version
$this->bannerLocationText = 'off';
$this->bannerLocationHtml = $action['value'];
} else {
$this->bannerLocationText = $action['value'];
}
}
break;
case 'block':
// Do nothing, already checked.
break;
case 'removeAttachments':
$this->emailAttachments = [];
break;
case 'forwardTo':
// Do nothing, already checked.
break;
case 'webhook':
// http payload to url
break;
}
}
}

View File

@ -1,184 +0,0 @@
<?php
namespace App\Traits;
use Illuminate\Support\Str;
trait CheckUserRules
{
protected $emailType;
public function checkRules(string $emailType)
{
$this->emailType = $emailType;
$method = "activeRulesFor{$emailType}Ordered";
$this->user->{$method}->each(function ($rule) {
// Check if the conditions of the rule are satisfied
if ($this->ruleConditionsSatisfied($rule->conditions, $rule->operator)) {
// Apply actions for that rule
collect($rule->actions)->each(function ($action) {
$this->applyAction($action);
});
// Increment applied count
$rule->increment('applied', 1, ['last_applied' => now()]);
}
});
}
protected function ruleConditionsSatisfied($conditions, $logicalOperator)
{
$results = collect();
collect($conditions)->each(function ($condition) use ($results) {
$results->push($this->lookupConditionType($condition));
});
$result = $results->unique();
if ($logicalOperator === 'OR') {
return $result->contains(true);
}
// Logical operator is AND so return false if any conditions are not met
return ! $result->contains(false);
}
protected function lookupConditionType($condition)
{
switch ($condition['type']) {
case 'sender':
return $this->conditionSatisfied($this->sender, $condition);
break;
case 'subject':
return $this->conditionSatisfied($this->subject, $condition);
break;
case 'alias':
return $this->conditionSatisfied($this->alias->email, $condition);
break;
case 'alias_description':
return $this->conditionSatisfied($this->alias->description, $condition);
break;
}
}
protected function conditionSatisfied($variable, $condition)
{
$values = collect($condition['values']);
switch ($condition['match']) {
case 'is exactly':
return $values->contains(function ($value) use ($variable) {
return $variable === $value;
});
break;
case 'is not':
return ! $values->contains(function ($value) use ($variable) {
return $variable === $value;
});
break;
case 'contains':
return $values->contains(function ($value) use ($variable) {
return Str::contains($variable, $value);
});
break;
case 'does not contain':
return ! $values->contains(function ($value) use ($variable) {
return Str::contains($variable, $value);
});
break;
case 'starts with':
return $values->contains(function ($value) use ($variable) {
return Str::startsWith($variable, $value);
});
break;
case 'does not start with':
return ! $values->contains(function ($value) use ($variable) {
return Str::startsWith($variable, $value);
});
break;
case 'ends with':
return $values->contains(function ($value) use ($variable) {
return Str::endsWith($variable, $value);
});
break;
case 'does not end with':
return ! $values->contains(function ($value) use ($variable) {
return Str::endsWith($variable, $value);
});
break;
case 'matches regex':
return $values->contains(function ($value) use ($variable) {
return Str::isMatch("/{$value}/", $variable);
});
break;
case 'does not match regex':
return ! $values->contains(function ($value) use ($variable) {
return Str::isMatch("/{$value}/", $variable);
});
break;
}
}
protected function applyAction($action)
{
switch ($action['type']) {
case 'subject':
$this->replacedSubject = ' with subject "'.base64_decode($this->emailSubject).'"';
$this->email->subject = $action['value'];
break;
case 'displayFrom':
$this->email->from = [];
$this->email->from($this->fromEmail, $action['value']);
break;
case 'encryption':
if ($action['value'] == false) {
if (isset($this->fingerprint)) {
$this->fingerprint = null;
}
}
break;
case 'banner':
if (in_array($action['value'], ['top', 'bottom', 'off'])) {
if ($this->emailHtml) {
// Turn off the banner for the plain text version
$this->bannerLocationText = 'off';
$this->bannerLocationHtml = $action['value'];
} else {
$this->bannerLocationText = $action['value'];
}
}
break;
case 'block':
$this->alias->increment('emails_blocked', 1, ['last_blocked' => now()]);
$this->size = 0;
exit(0);
break;
case 'removeAttachments':
$this->emailAttachments = [];
break;
case 'forwardTo':
// Only apply on forwards
if ($this->emailType !== 'Forwards') {
break;
}
$recipient = $this->user->verifiedRecipients()->select(['id', 'email', 'should_encrypt', 'fingerprint'])->find($action['value']);
if (! $recipient) {
break;
}
$this->recipientId = $recipient->id;
$this->fingerprint = $recipient->should_encrypt && ! $this->isAlreadyEncrypted() ? $recipient->fingerprint : null;
$this->email->to[0]['address'] = $recipient->email;
break;
case 'webhook':
// http payload to url
break;
}
}
}

62
composer.lock generated
View File

@ -1899,16 +1899,16 @@
},
{
"name": "laravel/framework",
"version": "v12.32.1",
"version": "v12.34.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "d41e4ba47706ec85bda85fe9362515592871cd21"
"reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/d41e4ba47706ec85bda85fe9362515592871cd21",
"reference": "d41e4ba47706ec85bda85fe9362515592871cd21",
"url": "https://api.github.com/repos/laravel/framework/zipball/f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687",
"reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687",
"shasum": ""
},
"require": {
@ -2020,7 +2020,7 @@
"league/flysystem-sftp-v3": "^3.25.1",
"mockery/mockery": "^1.6.10",
"opis/json-schema": "^2.4.1",
"orchestra/testbench-core": "^10.6.5",
"orchestra/testbench-core": "^10.7.0",
"pda/pheanstalk": "^5.0.6|^7.0.0",
"php-http/discovery": "^1.15",
"phpstan/phpstan": "^2.0",
@ -2114,7 +2114,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-09-30T12:22:56+00:00"
"time": "2025-10-14T13:58:31+00:00"
},
{
"name": "laravel/prompts",
@ -2241,16 +2241,16 @@
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.5",
"version": "v2.0.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
"reference": "3832547db6e0e2f8bb03d4093857b378c66eceed"
"reference": "038ce42edee619599a1debb7e81d7b3759492819"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3832547db6e0e2f8bb03d4093857b378c66eceed",
"reference": "3832547db6e0e2f8bb03d4093857b378c66eceed",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819",
"reference": "038ce42edee619599a1debb7e81d7b3759492819",
"shasum": ""
},
"require": {
@ -2298,7 +2298,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
"time": "2025-09-22T17:29:40+00:00"
"time": "2025-10-09T13:42:30+00:00"
},
{
"name": "laravel/tinker",
@ -3248,16 +3248,16 @@
},
{
"name": "mews/captcha",
"version": "3.4.6",
"version": "3.4.7",
"source": {
"type": "git",
"url": "https://github.com/mewebstudio/captcha.git",
"reference": "cead591ddc544b2b80a4136897893dd3bd70e7a7"
"reference": "2622c4f90dd621f19fe57e03e45f6f099509e839"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mewebstudio/captcha/zipball/cead591ddc544b2b80a4136897893dd3bd70e7a7",
"reference": "cead591ddc544b2b80a4136897893dd3bd70e7a7",
"url": "https://api.github.com/repos/mewebstudio/captcha/zipball/2622c4f90dd621f19fe57e03e45f6f099509e839",
"reference": "2622c4f90dd621f19fe57e03e45f6f099509e839",
"shasum": ""
},
"require": {
@ -3315,9 +3315,9 @@
],
"support": {
"issues": "https://github.com/mewebstudio/captcha/issues",
"source": "https://github.com/mewebstudio/captcha/tree/3.4.6"
"source": "https://github.com/mewebstudio/captcha/tree/3.4.7"
},
"time": "2025-04-16T15:40:08+00:00"
"time": "2025-10-11T14:42:33+00:00"
},
{
"name": "monolog/monolog",
@ -11440,16 +11440,16 @@
},
{
"name": "spatie/laravel-ray",
"version": "1.40.2",
"version": "1.41.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ray.git",
"reference": "1d1b31eb83cb38b41975c37363c7461de6d86b25"
"reference": "7b9cfdb024a390171397c14dbf321727d940bac5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-ray/zipball/1d1b31eb83cb38b41975c37363c7461de6d86b25",
"reference": "1d1b31eb83cb38b41975c37363c7461de6d86b25",
"url": "https://api.github.com/repos/spatie/laravel-ray/zipball/7b9cfdb024a390171397c14dbf321727d940bac5",
"reference": "7b9cfdb024a390171397c14dbf321727d940bac5",
"shasum": ""
},
"require": {
@ -11469,9 +11469,9 @@
"guzzlehttp/guzzle": "^7.3",
"laravel/framework": "^7.20 || ^8.19 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
"orchestra/testbench-core": "^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0",
"pestphp/pest": "^1.22 || ^2.0 || ^3.0",
"pestphp/pest": "^1.22 || ^2.0 || ^3.0 || ^4.0",
"phpstan/phpstan": "^1.10.57 || ^2.0.2",
"phpunit/phpunit": "^9.3 || ^10.1 || ^11.0.10",
"phpunit/phpunit": "^9.3 || ^10.1 || ^11.0.10 || ^12.4",
"rector/rector": "^0.19.2 || ^1.0.1 || ^2.0.0",
"spatie/pest-plugin-snapshots": "^1.1 || ^2.0",
"symfony/var-dumper": "^4.2 || ^5.1 || ^6.0 || ^7.0.3"
@ -11512,7 +11512,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-ray/issues",
"source": "https://github.com/spatie/laravel-ray/tree/1.40.2"
"source": "https://github.com/spatie/laravel-ray/tree/1.41.0"
},
"funding": [
{
@ -11524,7 +11524,7 @@
"type": "other"
}
],
"time": "2025-03-27T08:26:55+00:00"
"time": "2025-10-16T10:19:22+00:00"
},
{
"name": "spatie/macroable",
@ -11578,16 +11578,16 @@
},
{
"name": "spatie/ray",
"version": "1.42.0",
"version": "1.43.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/ray.git",
"reference": "152250ce7c490bf830349fa30ba5200084e95860"
"reference": "ee9c0477e0b7e5ff49e62cd1f4f75be27da91dbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/ray/zipball/152250ce7c490bf830349fa30ba5200084e95860",
"reference": "152250ce7c490bf830349fa30ba5200084e95860",
"url": "https://api.github.com/repos/spatie/ray/zipball/ee9c0477e0b7e5ff49e62cd1f4f75be27da91dbe",
"reference": "ee9c0477e0b7e5ff49e62cd1f4f75be27da91dbe",
"shasum": ""
},
"require": {
@ -11647,7 +11647,7 @@
],
"support": {
"issues": "https://github.com/spatie/ray/issues",
"source": "https://github.com/spatie/ray/tree/1.42.0"
"source": "https://github.com/spatie/ray/tree/1.43.0"
},
"funding": [
{
@ -11659,7 +11659,7 @@
"type": "other"
}
],
"time": "2025-04-18T08:17:40+00:00"
"time": "2025-10-16T10:01:03+00:00"
},
{
"name": "staabm/side-effects-detector",

412
package-lock.json generated
View File

@ -108,9 +108,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
"integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
"integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
"cpu": [
"ppc64"
],
@ -125,9 +125,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
"integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
"integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
"cpu": [
"arm"
],
@ -142,9 +142,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
"integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
"integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
"cpu": [
"arm64"
],
@ -159,9 +159,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
"integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
"integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
"cpu": [
"x64"
],
@ -176,9 +176,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
"integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
"integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
"cpu": [
"arm64"
],
@ -193,9 +193,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
"integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
"integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
"cpu": [
"x64"
],
@ -210,9 +210,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
"integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
"integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
"cpu": [
"arm64"
],
@ -227,9 +227,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
"integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
"integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
"cpu": [
"x64"
],
@ -244,9 +244,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
"integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
"integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
"cpu": [
"arm"
],
@ -261,9 +261,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
"integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
"integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
"cpu": [
"arm64"
],
@ -278,9 +278,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
"integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
"integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
"cpu": [
"ia32"
],
@ -295,9 +295,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
"integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
"integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
"cpu": [
"loong64"
],
@ -312,9 +312,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
"integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
"integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
"cpu": [
"mips64el"
],
@ -329,9 +329,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
"integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
"integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
"cpu": [
"ppc64"
],
@ -346,9 +346,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
"integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
"integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
"cpu": [
"riscv64"
],
@ -363,9 +363,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
"integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
"integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
"cpu": [
"s390x"
],
@ -380,9 +380,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
"integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
"integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
"cpu": [
"x64"
],
@ -397,9 +397,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
"integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz",
"integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==",
"cpu": [
"arm64"
],
@ -414,9 +414,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
"integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
"integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
"cpu": [
"x64"
],
@ -431,9 +431,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
"integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz",
"integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==",
"cpu": [
"arm64"
],
@ -448,9 +448,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
"integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
"integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
"cpu": [
"x64"
],
@ -465,9 +465,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
"integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz",
"integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==",
"cpu": [
"arm64"
],
@ -482,9 +482,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
"integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
"integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
"cpu": [
"x64"
],
@ -499,9 +499,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
"integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
"integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
"cpu": [
"arm64"
],
@ -516,9 +516,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
"integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
"integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
"cpu": [
"ia32"
],
@ -533,9 +533,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
"integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
"integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
"cpu": [
"x64"
],
@ -574,9 +574,9 @@
}
},
"node_modules/@inertiajs/core": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.2.3.tgz",
"integrity": "sha512-+aUIBGCRnRadACG5++5HBS41CvKf4z+DTnINQGjr1ToeiD6xPbXM4D+rKweSfSR+wgmL5TIdyeRM7vYJ3v57qQ==",
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.2.8.tgz",
"integrity": "sha512-sN6ArjsDajohctU7j17lsnSk5j3otDDSuR0JgtJxUftNcV3mv51XTKm/IHM3zdHYhq4rpvgHlr9epUqp7tMnxQ==",
"license": "MIT",
"dependencies": {
"@types/lodash-es": "^4.17.12",
@ -586,12 +586,12 @@
}
},
"node_modules/@inertiajs/vue3": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.2.3.tgz",
"integrity": "sha512-XEbnPE/NDNhploGMP4S/55acltxtAA+ddq8yOvZisZls0kNaGhrJYF80Yeo4uD1N9GyqtfFsAjldPOcUocnUtw==",
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.2.8.tgz",
"integrity": "sha512-/UuXIsreUF/bMnzYtv/faYJeQGJ2W1AnJhQCtMpJiSUZS9bhZuM3DWspJfdKsL9G7BV9LaVVm+Z+uER8SEZQDg==",
"license": "MIT",
"dependencies": {
"@inertiajs/core": "2.2.3",
"@inertiajs/core": "2.2.8",
"@types/lodash-es": "^4.17.12",
"lodash-es": "^4.17.21"
},
@ -861,13 +861,13 @@
}
},
"node_modules/@types/node": {
"version": "24.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.0.tgz",
"integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==",
"version": "24.7.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.13.0"
"undici-types": "~7.14.0"
}
},
"node_modules/@vitejs/plugin-vue": {
@ -1317,24 +1317,6 @@
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"license": "MIT"
},
"node_modules/async-function": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
"integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/async-generator-function": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-generator-function/-/async-generator-function-1.0.0.tgz",
"integrity": "sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -1396,9 +1378,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz",
"integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
"integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@ -1447,9 +1429,9 @@
}
},
"node_modules/browserslist": {
"version": "4.26.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
"version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"funding": [
{
"type": "opencollective",
@ -1466,9 +1448,9 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
"electron-to-chromium": "^1.5.218",
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
"electron-to-chromium": "^1.5.227",
"node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
},
@ -1525,9 +1507,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001746",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz",
"integrity": "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==",
"version": "1.0.30001751",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
"funding": [
{
"type": "opencollective",
@ -1558,9 +1540,9 @@
}
},
"node_modules/chart.js": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
@ -1875,15 +1857,15 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.227",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz",
"integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==",
"version": "1.5.237",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
"integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==",
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz",
"integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==",
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
@ -1988,9 +1970,9 @@
}
},
"node_modules/esbuild": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
"integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
"integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -2001,32 +1983,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.10",
"@esbuild/android-arm": "0.25.10",
"@esbuild/android-arm64": "0.25.10",
"@esbuild/android-x64": "0.25.10",
"@esbuild/darwin-arm64": "0.25.10",
"@esbuild/darwin-x64": "0.25.10",
"@esbuild/freebsd-arm64": "0.25.10",
"@esbuild/freebsd-x64": "0.25.10",
"@esbuild/linux-arm": "0.25.10",
"@esbuild/linux-arm64": "0.25.10",
"@esbuild/linux-ia32": "0.25.10",
"@esbuild/linux-loong64": "0.25.10",
"@esbuild/linux-mips64el": "0.25.10",
"@esbuild/linux-ppc64": "0.25.10",
"@esbuild/linux-riscv64": "0.25.10",
"@esbuild/linux-s390x": "0.25.10",
"@esbuild/linux-x64": "0.25.10",
"@esbuild/netbsd-arm64": "0.25.10",
"@esbuild/netbsd-x64": "0.25.10",
"@esbuild/openbsd-arm64": "0.25.10",
"@esbuild/openbsd-x64": "0.25.10",
"@esbuild/openharmony-arm64": "0.25.10",
"@esbuild/sunos-x64": "0.25.10",
"@esbuild/win32-arm64": "0.25.10",
"@esbuild/win32-ia32": "0.25.10",
"@esbuild/win32-x64": "0.25.10"
"@esbuild/aix-ppc64": "0.25.11",
"@esbuild/android-arm": "0.25.11",
"@esbuild/android-arm64": "0.25.11",
"@esbuild/android-x64": "0.25.11",
"@esbuild/darwin-arm64": "0.25.11",
"@esbuild/darwin-x64": "0.25.11",
"@esbuild/freebsd-arm64": "0.25.11",
"@esbuild/freebsd-x64": "0.25.11",
"@esbuild/linux-arm": "0.25.11",
"@esbuild/linux-arm64": "0.25.11",
"@esbuild/linux-ia32": "0.25.11",
"@esbuild/linux-loong64": "0.25.11",
"@esbuild/linux-mips64el": "0.25.11",
"@esbuild/linux-ppc64": "0.25.11",
"@esbuild/linux-riscv64": "0.25.11",
"@esbuild/linux-s390x": "0.25.11",
"@esbuild/linux-x64": "0.25.11",
"@esbuild/netbsd-arm64": "0.25.11",
"@esbuild/netbsd-x64": "0.25.11",
"@esbuild/openbsd-arm64": "0.25.11",
"@esbuild/openbsd-x64": "0.25.11",
"@esbuild/openharmony-arm64": "0.25.11",
"@esbuild/sunos-x64": "0.25.11",
"@esbuild/win32-arm64": "0.25.11",
"@esbuild/win32-ia32": "0.25.11",
"@esbuild/win32-x64": "0.25.11"
}
},
"node_modules/escalade": {
@ -2293,15 +2275,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generator-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.0.tgz",
"integrity": "sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
@ -2316,19 +2289,16 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.1.tgz",
"integrity": "sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"async-function": "^1.0.0",
"async-generator-function": "^1.0.0",
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"generator-function": "^2.0.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
@ -2765,13 +2735,17 @@
}
},
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.11.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/loader-utils": {
@ -3036,9 +3010,9 @@
"peer": true
},
"node_modules/node-releases": {
"version": "2.0.21",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
"version": "2.0.25",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz",
"integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==",
"license": "MIT"
},
"node_modules/normalize-path": {
@ -3291,9 +3265,9 @@
}
},
"node_modules/postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"funding": [
{
"type": "opencollective",
@ -3306,21 +3280,28 @@
],
"license": "MIT",
"dependencies": {
"lilconfig": "^3.0.0",
"yaml": "^2.3.4"
"lilconfig": "^3.1.1"
},
"engines": {
"node": ">= 14"
"node": ">= 18"
},
"peerDependencies": {
"jiti": ">=1.21.0",
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
},
"postcss": {
"optional": true
},
"ts-node": {
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
@ -3638,9 +3619,9 @@
},
"node_modules/rollup": {
"name": "@rollup/wasm-node",
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.52.3.tgz",
"integrity": "sha512-Vltzfan6IBSm4dG3w8ArFVUMhBABbW/9uYMPnbYyv2Vk+Jry9qzlXKvxSZhDbvwtb0GJHDWwPOMj6d8G2cb9Tw==",
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.52.4.tgz",
"integrity": "sha512-QME8thp2j0GvRu/H8kz3uOawi45rexNIys38kITnMYp8Wl+gyeoIIuKyw8y0Lrq6xSAXgGCoqDyHD+m0wX1jnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3702,9 +3683,9 @@
"peer": true
},
"node_modules/schema-utils": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -3722,9 +3703,9 @@
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
@ -4090,9 +4071,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"version": "3.4.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@ -4103,7 +4084,7 @@
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.6",
"jiti": "^1.21.7",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
@ -4112,7 +4093,7 @@
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2",
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
@ -4157,9 +4138,9 @@
}
},
"node_modules/tapable": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"license": "MIT",
"peer": true,
"engines": {
@ -4328,9 +4309,9 @@
"license": "Apache-2.0"
},
"node_modules/undici-types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
"integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"license": "MIT",
"peer": true
},
@ -4371,9 +4352,9 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.0.tgz",
"integrity": "sha512-oLnWs9Hak/LOlKjeSpOwD6JMks8BeICEdYMJBf6P4Lac/pO9tKiv/XhXnAM7nNfSkZahjlCZu9sS50zL8fSnsw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4629,9 +4610,9 @@
}
},
"node_modules/webpack": {
"version": "5.102.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.0.tgz",
"integrity": "sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==",
"version": "5.102.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -4643,7 +4624,7 @@
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.24.5",
"browserslist": "^4.26.3",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
@ -4655,8 +4636,8 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.2",
"tapable": "^2.2.3",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.4",
"webpack-sources": "^3.3.3"
@ -4807,6 +4788,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"devOptional": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"

32
postfix/composer.lock generated
View File

@ -289,7 +289,7 @@
},
{
"name": "illuminate/collections",
"version": "v11.46.0",
"version": "v11.46.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
@ -345,7 +345,7 @@
},
{
"name": "illuminate/conditionable",
"version": "v11.46.0",
"version": "v11.46.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
@ -391,7 +391,7 @@
},
{
"name": "illuminate/container",
"version": "v11.46.0",
"version": "v11.46.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
@ -442,7 +442,7 @@
},
{
"name": "illuminate/contracts",
"version": "v11.46.0",
"version": "v11.46.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@ -490,16 +490,16 @@
},
{
"name": "illuminate/database",
"version": "v11.46.0",
"version": "v11.46.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/database.git",
"reference": "6ba0589ddfd61a05989d00d138bd6c625f75dc33"
"reference": "96abcce13f405701363d916dd312835e04848d04"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/database/zipball/6ba0589ddfd61a05989d00d138bd6c625f75dc33",
"reference": "6ba0589ddfd61a05989d00d138bd6c625f75dc33",
"url": "https://api.github.com/repos/illuminate/database/zipball/96abcce13f405701363d916dd312835e04848d04",
"reference": "96abcce13f405701363d916dd312835e04848d04",
"shasum": ""
},
"require": {
@ -555,11 +555,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-09-02T23:55:36+00:00"
"time": "2025-09-29T09:23:31+00:00"
},
{
"name": "illuminate/macroable",
"version": "v11.46.0",
"version": "v11.46.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
@ -605,7 +605,7 @@
},
{
"name": "illuminate/support",
"version": "v11.46.0",
"version": "v11.46.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
@ -682,16 +682,16 @@
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.5",
"version": "v2.0.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
"reference": "3832547db6e0e2f8bb03d4093857b378c66eceed"
"reference": "038ce42edee619599a1debb7e81d7b3759492819"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3832547db6e0e2f8bb03d4093857b378c66eceed",
"reference": "3832547db6e0e2f8bb03d4093857b378c66eceed",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819",
"reference": "038ce42edee619599a1debb7e81d7b3759492819",
"shasum": ""
},
"require": {
@ -739,7 +739,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
"time": "2025-09-22T17:29:40+00:00"
"time": "2025-10-09T13:42:30+00:00"
},
{
"name": "nesbot/carbon",

View File

@ -734,7 +734,7 @@
</button>
<button
@click="closeDisableKeyModal"
class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
class="px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Close
</button>
@ -787,7 +787,7 @@
</button>
<button
@click="closeDeleteKeyModal"
class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
class="px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Close
</button>

View File

@ -1,7 +1,7 @@
@if($locationHtml === 'off' && ! $isSpam)
{!! $html !!}
@else
<table style="width:100%;">
<table style="width:100% !important;">
<tbody>
@if($isSpam)
@include('emails.forward.html_spam_warning')
@ -10,7 +10,7 @@
@include('emails.forward.html_banner')
@endif
<tr>
<td style="padding:10px 0;width:100%;">
<td style="padding:10px 0 !important;width:100% !important;">
{!! $html !!}
</td>
</tr>

View File

@ -1,7 +1,7 @@
<tr>
<td>
<div style="margin:0px auto;max-width:896px;padding:10px 20px;background-color:#f5f7fa;text-align:center;line-height:1.5;font-size:12px;width:100%;border-left: 3px solid #19216c;font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';color:#323f4b;overflow-wrap:break-word;box-sizing:border-box;">
This email was sent to <span style="font-weight:500;color:#19216c;">{{ $aliasEmail }}</span>{{ $aliasDescription ? ' (' . $aliasDescription . ')' : '' }} from <span style="font-weight:500;color:#19216c;">{{ $fromEmail }}</span>{{ $replacedSubject }}<br>Click <a href="{{ $deactivateUrl }}" style="color:#2d3a8c;text-decoration:underline;" target="_blank" rel="noreferrer noopener nofollow">here</a> to deactivate this alias
<div style="margin:0px auto !important;max-width:896px !important;padding:10px 20px !important;background-color:#f5f7fa !important;text-align:center !important;line-height:1.5 !important;font-size:12px !important;width:100% !important;border-left: 3px solid #19216c !important;font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' !important;color:#323f4b !important;overflow-wrap:break-word !important;box-sizing:border-box !important;">
This email was sent to <span style="font-weight:500 !important;color:#19216c !important;">{{ $aliasEmail }}</span>{{ $aliasDescription ? ' (' . $aliasDescription . ')' : '' }} from <span style="font-weight:500 !important;color:#19216c !important;">{{ $fromEmail }}</span>{{ $replacedSubject }}<br>Click <a href="{{ $deactivateUrl }}" style="color:#2d3a8c !important;text-decoration:underline !important;" target="_blank" rel="noreferrer noopener nofollow">here</a> to deactivate this alias
</div>
</td>
</tr>

View File

@ -1,7 +1,11 @@
<tr>
<td>
<div style="margin:0px auto;width:100%;padding:10px 20px;background-color:#CF1124;text-align:center;line-height:1.5;font-size:14px;font-weight:500;font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';color:#ffffff;overflow-wrap:break-word;">
Warning: This email may be spoofed or improperly forwarded, please check the 'X-AnonAddy-Authentication-Results' header.
<div style="margin:0px auto !important;width:100% !important;padding:10px 20px !important;background-color:#CF1124 !important;text-align:center !important;line-height:1.5 !important;font-size:14px !important;font-weight:500 !important;font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' !important;color:#ffffff !important;overflow-wrap:break-word !important;">
@if ($failedDmarc)
Warning: This email has failed its domain's authentication requirements. It may be spoofed or improperly forwarded.
@else
Warning: This email has a high spam score. It may be unsolicited, promotional, or contain malicious content.
@endif
</div>
</td>
</tr>

View File

@ -4,6 +4,7 @@ namespace Tests\Feature\Api;
use App\Models\Domain;
use App\Models\Recipient;
use App\Notifications\CustomVerifyEmail;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Notification;
use PHPUnit\Framework\Attributes\Test;

View File

@ -6,6 +6,7 @@ use App\Mail\ForwardEmail;
use App\Models\Alias;
use App\Models\EmailData;
use App\Models\Rule;
use App\Services\UserRuleChecker;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Str;
use PhpMimeMailParser\Parser;
@ -267,7 +268,12 @@ class RulesTest extends TestCase
$emailData = new EmailData($parser, $sender, $size);
$job = new ForwardEmail($alias, $emailData, $this->user->defaultRecipient);
// Check user rules and get rule IDs that have satisfied conditions
$ruleIdsAndActions = UserRuleChecker::getRuleIdsAndActionsForForwards($this->user, $emailData, $alias);
$ruleIds = array_keys($ruleIdsAndActions);
$job = new ForwardEmail($alias, $emailData, $this->user->defaultRecipient, false, $ruleIds);
$email = $job->build();
@ -336,7 +342,11 @@ class RulesTest extends TestCase
$emailData = new EmailData($parser, $sender, $size);
$job = new ForwardEmail($alias, $emailData, $this->user->defaultRecipient);
// Check user rules and get rule IDs that have satisfied conditions
$ruleIdsAndActions = UserRuleChecker::getRuleIdsAndActionsForForwards($this->user, $emailData, $alias);
$ruleIds = array_keys($ruleIdsAndActions);
$job = new ForwardEmail($alias, $emailData, $this->user->defaultRecipient, false, $ruleIds);
$email = $job->build();
@ -429,7 +439,11 @@ class RulesTest extends TestCase
$emailData = new EmailData($parser, $sender, $size);
$job = new ForwardEmail($alias, $emailData, $this->user->defaultRecipient);
// Check user rules and get rule IDs that have satisfied conditions
$ruleIdsAndActions = UserRuleChecker::getRuleIdsAndActionsForForwards($this->user, $emailData, $alias);
$ruleIds = array_keys($ruleIdsAndActions);
$job = new ForwardEmail($alias, $emailData, $this->user->defaultRecipient, false, $ruleIds);
$email = $job->build();