mirror of
https://github.com/anonaddy/anonaddy
synced 2026-04-25 17:15:29 +02:00
Added inbound rejections to failed deliveries
This commit is contained in:
@@ -79,6 +79,9 @@ ANONADDY_PROXY_AUTHENTICATION_EMAIL_HEADER=X-Email
|
|||||||
BLOCKLIST_API_ALLOWED_IPS=127.0.0.1
|
BLOCKLIST_API_ALLOWED_IPS=127.0.0.1
|
||||||
BLOCKLIST_API_SECRET=uStu5BoDvDoxGi1AHCfEW3ougDcgty
|
BLOCKLIST_API_SECRET=uStu5BoDvDoxGi1AHCfEW3ougDcgty
|
||||||
|
|
||||||
|
# If the path to your Postfix mail log file is not /var/log/mail.log then you can set it here
|
||||||
|
#POSTFIX_LOG_PATH=/path/to/mail.log
|
||||||
|
|
||||||
# Optional: override word/name lists with a comma-separated list or path to a PHP file that returns an array
|
# Optional: override word/name lists with a comma-separated list or path to a PHP file that returns an array
|
||||||
# ANONADDY_BLACKLIST=reserved,admin,root
|
# ANONADDY_BLACKLIST=reserved,admin,root
|
||||||
# ANONADDY_MALE_FIRST_NAMES=config/lists/custom_male_first.php
|
# ANONADDY_MALE_FIRST_NAMES=config/lists/custom_male_first.php
|
||||||
|
|||||||
146
SELF-HOSTING.md
146
SELF-HOSTING.md
@@ -307,6 +307,35 @@ Making sure to replace `johndoe` with the username of the user who will run the
|
|||||||
|
|
||||||
This command will pipe the email through to our application so that we can determine who the alias belongs to and who to forward the email to.
|
This command will pipe the email through to our application so that we can determine who the alias belongs to and who to forward the email to.
|
||||||
|
|
||||||
|
In order to parse the Postfix log files and display inbound rejections to your aliases you will need to give your `johndoe` user permission to view the mail log:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setfacl -m u:johndoe:r /var/log/mail.log
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also need to add the following to the `postrotate` hook of logrotate so that this is reapplied when you mail log is rotated:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo vim /etc/logrotate.d/rsyslog
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/log/mail.log
|
||||||
|
{
|
||||||
|
...
|
||||||
|
postrotate
|
||||||
|
/usr/lib/rsyslog/rsyslog-rotate
|
||||||
|
[ -f /var/log/mail.log ] && setfacl -m u:johndoe:r /var/log/mail.log
|
||||||
|
endscript
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If your mail log is at a location other than `/var/log/mail.log` you can set this in your `.env` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
POSTFIX_LOG_PATH=/path/to/mail.log
|
||||||
|
```
|
||||||
|
|
||||||
## Installing Nginx
|
## Installing Nginx
|
||||||
|
|
||||||
To install Nginx first add the prerequisites add then add the following signing key and repo (instructions taken from [nginx.org](https://nginx.org/en/linux_packages.html#Ubuntu)).
|
To install Nginx first add the prerequisites add then add the following signing key and repo (instructions taken from [nginx.org](https://nginx.org/en/linux_packages.html#Ubuntu)).
|
||||||
@@ -895,6 +924,111 @@ Restart Rspamd to reflect the changes.
|
|||||||
sudo service rspamd restart
|
sudo service rspamd restart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Now add support for the user blocklist feature by creating `/etc/rspamd/lua.local.d/addy_blocklist.lua`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/rspamd/lua.local.d/addy_blocklist.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
--[[
|
||||||
|
Rspamd Lua script: user blocklist check via Laravel HTTP API.
|
||||||
|
|
||||||
|
Deploy this on each mail server that runs Rspamd. Point blocklist_api_url
|
||||||
|
at your Laravel app.
|
||||||
|
|
||||||
|
Required: rspamd_http (built-in). Symbol BLOCKLIST_USER is set when the
|
||||||
|
API returns block=true; map this symbol to an action (reject/discard) in
|
||||||
|
your Rspamd actions config.
|
||||||
|
--]]
|
||||||
|
|
||||||
|
local blocklist_api_url = 'https://your-addy-instance.com/api/blocklist-check'
|
||||||
|
local blocklist_secret = '' -- same as BLOCKLIST_API_SECRET in .env, or leave '' if unset
|
||||||
|
|
||||||
|
-- Simple percent-encode for query parameter values (rspamd_http has no escape)
|
||||||
|
local function url_encode(s)
|
||||||
|
if s == nil or s == '' then return '' end
|
||||||
|
s = tostring(s)
|
||||||
|
return (s:gsub('[^%w%-_.~ ]', function(c)
|
||||||
|
return string.format('%%%02X', string.byte(c))
|
||||||
|
end):gsub(' ', '%%20'))
|
||||||
|
end
|
||||||
|
|
||||||
|
local logger = require "rspamd_logger"
|
||||||
|
local rspamd_http = require 'rspamd_http'
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'BLOCKLIST_USER',
|
||||||
|
callback = function(task)
|
||||||
|
local rcpts = task:get_recipients('smtp')
|
||||||
|
local from_env = task:get_from('smtp')
|
||||||
|
if not rcpts or #rcpts == 0 then
|
||||||
|
logger.infox('blocklist: skip - missing recipient')
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local recipient = (rcpts[1].addr and rcpts[1].addr:lower()) or ''
|
||||||
|
|
||||||
|
local sender = ''
|
||||||
|
if from_env and from_env.addr then
|
||||||
|
sender = from_env.addr:lower()
|
||||||
|
end
|
||||||
|
|
||||||
|
local from_email = ''
|
||||||
|
local from_hdr = task:get_header('From')
|
||||||
|
if from_hdr then
|
||||||
|
local raw = (type(from_hdr) == 'table') and (from_hdr[1] or from_hdr) or from_hdr
|
||||||
|
raw = tostring(raw)
|
||||||
|
from_email = raw:match('<([^>]+)>') or raw:match('%S+@%S+') or ''
|
||||||
|
from_email = from_email:lower()
|
||||||
|
end
|
||||||
|
if from_email == '' then
|
||||||
|
from_email = sender
|
||||||
|
end
|
||||||
|
|
||||||
|
if recipient == '' or (sender == '' and from_email == '') then
|
||||||
|
logger.infox('blocklist: skip - missing recipient or from (recipient=%1, sender=%2, from_email=%3)', recipient, sender, from_email)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local url = blocklist_api_url
|
||||||
|
.. '?recipient=' .. url_encode(recipient)
|
||||||
|
.. '&from_email=' .. url_encode(from_email)
|
||||||
|
|
||||||
|
local req_headers = {}
|
||||||
|
if blocklist_secret ~= '' then
|
||||||
|
req_headers['X-Blocklist-Secret'] = blocklist_secret
|
||||||
|
end
|
||||||
|
|
||||||
|
rspamd_http.request({
|
||||||
|
url = url,
|
||||||
|
headers = req_headers,
|
||||||
|
timeout = 2.0,
|
||||||
|
task = task,
|
||||||
|
callback = function(err_message, code, body, _headers)
|
||||||
|
if err_message then
|
||||||
|
logger.warnx('blocklist: HTTP error - %1', err_message)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if code == 200 and body and body:match('"block"%s*:%s*true') then
|
||||||
|
task:set_pre_result('reject', '550 5.1.1 Address not found')
|
||||||
|
task:insert_result(true, 'BLOCKLIST_USER', 1000.0, '550 5.1.1 Address not found')
|
||||||
|
logger.infox('blocklist: BLOCKLIST_USER set for recipient=%1 from_email=%2', recipient, from_email)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
return false -- do not match symbol here; only HTTP callback may add it via insert_result
|
||||||
|
end,
|
||||||
|
score = 1000.0,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart Rspamd again after creating the file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo service rspamd restart
|
||||||
|
```
|
||||||
|
|
||||||
You can view the Rspamd web interface by creating an SSH tunnel by running the following command on your local pc:
|
You can view the Rspamd web interface by creating an SSH tunnel by running the following command on your local pc:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -959,7 +1093,7 @@ fi
|
|||||||
|
|
||||||
Make sure node is installed (`node -v`) if not then install it using NVM - [https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-22-04#option-3-installing-node-using-the-node-version-manager](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-22-04#option-3-installing-node-using-the-node-version-manager)
|
Make sure node is installed (`node -v`) if not then install it using NVM - [https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-22-04#option-3-installing-node-using-the-node-version-manager](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-22-04#option-3-installing-node-using-the-node-version-manager)
|
||||||
|
|
||||||
At the time of writing this I'm using the latest LTS - v18.18.2
|
You should use Node `20.19+` (required for Vite 8).
|
||||||
|
|
||||||
Next copy the .env.example file and update it with correct values (database password, app url, redis password etc.) then install the dependencies.
|
Next copy the .env.example file and update it with correct values (database password, app url, redis password etc.) then install the dependencies.
|
||||||
|
|
||||||
@@ -975,6 +1109,16 @@ npm run production
|
|||||||
|
|
||||||
Make sure to update the database settings, redis password and the AnonAddy variables. You can use Redis for queue, sessions and cache.
|
Make sure to update the database settings, redis password and the AnonAddy variables. You can use Redis for queue, sessions and cache.
|
||||||
|
|
||||||
|
Also add the blocklist API variables to your `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Blocklist API (Rspamd): comma-separated IPs allowed to call /api/blocklist-check; optional shared secret
|
||||||
|
BLOCKLIST_API_ALLOWED_IPS=127.0.0.1
|
||||||
|
BLOCKLIST_API_SECRET=
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure `BLOCKLIST_API_ALLOWED_IPS` includes the actual IP address(es) of your mail server(s). If you set a secret here, it must match `blocklist_secret` in `/etc/rspamd/lua.local.d/addy_blocklist.lua`.
|
||||||
|
|
||||||
We'll set `ANONADDY_SIGNING_KEY_FINGERPRINT` shortly.
|
We'll set `ANONADDY_SIGNING_KEY_FINGERPRINT` shortly.
|
||||||
|
|
||||||
`APP_KEY` will be generated in the next step, this is used by Laravel for securely encrypting values.
|
`APP_KEY` will be generated in the next step, this is used by Laravel for securely encrypting values.
|
||||||
|
|||||||
193
app/Console/Commands/ParsePostfixMailLog.php
Normal file
193
app/Console/Commands/ParsePostfixMailLog.php
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Alias;
|
||||||
|
use App\Models\FailedDelivery;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ParsePostfixMailLog extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'anonaddy:parse-postfix-mail-log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Parse Postfix log for inbound rejections and store them';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$logPath = config('anonaddy.postfix_log_path', '/var/log/mail.log');
|
||||||
|
|
||||||
|
if (! file_exists($logPath) || ! is_readable($logPath)) {
|
||||||
|
$this->error("Cannot read log file: {$logPath}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$positionFile = 'postfix_log_position.txt';
|
||||||
|
$lastPosition = Storage::disk('local')->exists($positionFile)
|
||||||
|
? (int) Storage::disk('local')->get($positionFile)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
$fileSize = filesize($logPath);
|
||||||
|
|
||||||
|
// If the file is smaller than our last position, it was likely rotated
|
||||||
|
if ($fileSize < $lastPosition) {
|
||||||
|
$lastPosition = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fileSize === $lastPosition) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($logPath, 'r');
|
||||||
|
if (! $handle) {
|
||||||
|
$this->error("Failed to open log file: {$logPath}");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fseek($handle, $lastPosition);
|
||||||
|
|
||||||
|
$allDomains = config('anonaddy.all_domains', []);
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
$storeErrors = 0;
|
||||||
|
|
||||||
|
// Pattern to match syslog and ISO8601 timestamps
|
||||||
|
$pattern = '/^(.*?)\s+(?:[^\s]+\s+)?postfix\/(?:smtpd|cleanup)\[\d+\]:\s*(?:[A-Z0-9]+:\s*)?(?:reject|milter-reject|discard):\s*(?:RCPT|END-OF-MESSAGE) from\s*([^:]+):\s*(?:<[^>]+>:\s*)?(?:(\d{3}\s+\d\.\d\.\d|\d\.\d\.\d)\s+)?(.*?);\s*from=<(.*?)>\s*to=<(.*?)>/i';
|
||||||
|
|
||||||
|
while (($line = fgets($handle)) !== false) {
|
||||||
|
if (! preg_match('/(?:reject:|milter-reject:|discard:)/', $line) || ! str_contains($line, 'to=<')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match($pattern, $line, $matches)) {
|
||||||
|
$timestampStr = trim($matches[1]);
|
||||||
|
$remoteMta = trim($matches[2]);
|
||||||
|
$smtpCodeStr = trim($matches[3] ?? '');
|
||||||
|
$reason = trim($matches[4]);
|
||||||
|
$sender = trim($matches[5]);
|
||||||
|
$recipient = trim($matches[6]);
|
||||||
|
|
||||||
|
$smtpCode = '';
|
||||||
|
if ($smtpCodeStr) {
|
||||||
|
$parts = explode(' ', $smtpCodeStr);
|
||||||
|
$smtpCode = $parts[0];
|
||||||
|
$reason = $smtpCodeStr.' '.$reason;
|
||||||
|
} elseif (preg_match('/^(\d{3})\s+(.*)$/', $reason, $reasonMatches)) {
|
||||||
|
$smtpCode = $reasonMatches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$attemptedAt = Carbon::parse($timestampStr);
|
||||||
|
if ($attemptedAt->isFuture()) {
|
||||||
|
$attemptedAt->subYear();
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$attemptedAt = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
$recipientLower = strtolower($recipient);
|
||||||
|
$aliasLookup = $recipientLower;
|
||||||
|
|
||||||
|
if (str_contains($recipientLower, '+')) {
|
||||||
|
$parts = explode('@', $recipientLower);
|
||||||
|
if (count($parts) === 2) {
|
||||||
|
$aliasLookup = explode('+', $parts[0])[0].'@'.$parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$alias = Alias::withTrashed()->where('email', $aliasLookup)->first();
|
||||||
|
$userId = null;
|
||||||
|
|
||||||
|
if ($alias) {
|
||||||
|
$userId = $alias->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $userId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bounceType = 'hard';
|
||||||
|
if (str_contains(strtolower($reason), 'spam message rejected')) {
|
||||||
|
$bounceType = 'spam';
|
||||||
|
}
|
||||||
|
|
||||||
|
$irDedupeKey = hash('sha256', $userId.'|'.($alias ? $alias->id : '').'|'.$attemptedAt->format('Y-m-d H:i:s'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
FailedDelivery::create([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'alias_id' => $alias ? $alias->id : null,
|
||||||
|
'email_type' => 'IR',
|
||||||
|
'ir_dedupe_key' => $irDedupeKey,
|
||||||
|
'sender' => $sender === '' ? '<>' : Str::limit($sender, 255),
|
||||||
|
'destination' => $recipientLower,
|
||||||
|
'remote_mta' => Str::limit($remoteMta, 255),
|
||||||
|
'code' => Str::limit($reason, 255),
|
||||||
|
'status' => $smtpCode ? Str::limit($smtpCode, 10) : null,
|
||||||
|
'attempted_at' => $attemptedAt,
|
||||||
|
'created_at' => $attemptedAt,
|
||||||
|
'updated_at' => $attemptedAt,
|
||||||
|
'bounce_type' => $bounceType,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
} catch (QueryException $e) {
|
||||||
|
if ($this->isDuplicateKeyException($e)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
report($e);
|
||||||
|
$storeErrors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$newPosition = ftell($handle);
|
||||||
|
Storage::disk('local')->put($positionFile, (string) $newPosition);
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
$this->info("Stored {$count} inbound rejections.");
|
||||||
|
Log::info("Stored {$count} inbound rejections.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($storeErrors > 0) {
|
||||||
|
$this->warn("Failed to store {$storeErrors} inbound rejection(s); see application log for details.");
|
||||||
|
Log::info("Failed to store {$storeErrors} inbound rejection(s); see application log for details.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isDuplicateKeyException(QueryException $e): bool
|
||||||
|
{
|
||||||
|
return match (DB::getDriverName()) {
|
||||||
|
'mysql' => ($e->errorInfo[1] ?? 0) === 1062,
|
||||||
|
'sqlite' => str_contains($e->getMessage(), 'UNIQUE constraint failed'),
|
||||||
|
default => ($e->errorInfo[0] ?? '') === '23000',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -403,10 +403,9 @@ class ReceiveEmail extends Command
|
|||||||
$forwardToRecipientIds = UserRuleChecker::getRecipientIdsToForwardToFromRuleIdsAndActions($ruleIdsAndActions);
|
$forwardToRecipientIds = UserRuleChecker::getRecipientIdsToForwardToFromRuleIdsAndActions($ruleIdsAndActions);
|
||||||
|
|
||||||
if (! empty($forwardToRecipientIds)) {
|
if (! empty($forwardToRecipientIds)) {
|
||||||
$recipients = $this->user->verifiedRecipients()->whereIn('id', $forwardToRecipientIds)->get();
|
$ruleRecipients = $this->user->verifiedRecipients()->whereIn('id', $forwardToRecipientIds)->get();
|
||||||
|
if ($ruleRecipients->isNotEmpty()) {
|
||||||
if ($recipients) {
|
$recipientsToForwardTo = $ruleRecipients;
|
||||||
$recipientsToForwardTo = $this->user->verifiedRecipients()->whereIn('id', $forwardToRecipientIds)->get();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,29 @@
|
|||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\IndexFailedDeliveryRequest;
|
||||||
use App\Http\Resources\FailedDeliveryResource;
|
use App\Http\Resources\FailedDeliveryResource;
|
||||||
|
|
||||||
class FailedDeliveryController extends Controller
|
class FailedDeliveryController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index(IndexFailedDeliveryRequest $request)
|
||||||
{
|
{
|
||||||
$failedDeliveries = user()->failedDeliveries()->with(['recipient:id,email', 'alias:id,email'])->latest();
|
$failedDeliveries = user()
|
||||||
|
->failedDeliveries()
|
||||||
|
->with(['recipient:id,email', 'alias:id,email'])
|
||||||
|
->when($request->input('filter.email_type'), function ($query, $value) {
|
||||||
|
if ($value === 'inbound') {
|
||||||
|
return $query->where('email_type', 'IR');
|
||||||
|
}
|
||||||
|
|
||||||
return FailedDeliveryResource::collection($failedDeliveries->get());
|
if ($value === 'outbound') {
|
||||||
|
return $query->where('email_type', '!=', 'IR');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->latest()
|
||||||
|
->jsonPaginate();
|
||||||
|
|
||||||
|
return FailedDeliveryResource::collection($failedDeliveries);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show($id)
|
public function show($id)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Alias;
|
use App\Models\Alias;
|
||||||
|
use App\Notifications\AliasDeactivatedByUnsubscribeNotification;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class DeactivateAliasController extends Controller
|
class DeactivateAliasController extends Controller
|
||||||
@@ -30,14 +33,35 @@ class DeactivateAliasController extends Controller
|
|||||||
->with(['flash' => 'Alias '.$alias->email.' deactivated successfully!']);
|
->with(['flash' => 'Alias '.$alias->email.' deactivated successfully!']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deactivatePost($id)
|
public function deactivatePost(Request $request, $id)
|
||||||
{
|
{
|
||||||
$alias = Alias::findOrFail($id);
|
$alias = Alias::findOrFail($id);
|
||||||
|
|
||||||
|
$wasActive = $alias->active;
|
||||||
|
|
||||||
$alias->deactivate();
|
$alias->deactivate();
|
||||||
|
|
||||||
Log::info('One-Click Unsubscribe deactivated alias: '.$alias->email.' ID: '.$id);
|
Log::info('One-Click Unsubscribe deactivated alias: '.$alias->email.' ID: '.$id);
|
||||||
|
|
||||||
|
if ($wasActive) {
|
||||||
|
$cacheKey = "unsubscribe-deactivate-notify:{$alias->id}";
|
||||||
|
|
||||||
|
if (! Cache::has($cacheKey)) {
|
||||||
|
Cache::put($cacheKey, true, now()->addHour());
|
||||||
|
|
||||||
|
$user = $alias->user;
|
||||||
|
$user->notify(
|
||||||
|
(new AliasDeactivatedByUnsubscribeNotification(
|
||||||
|
$alias->email,
|
||||||
|
$alias->id,
|
||||||
|
$request->ip(),
|
||||||
|
$request->userAgent(),
|
||||||
|
now()->format('F j, g:i A (T)'),
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response('');
|
return response('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Alias;
|
use App\Models\Alias;
|
||||||
|
use App\Notifications\AliasDeletedByUnsubscribeNotification;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class DeleteAliasController extends Controller
|
class DeleteAliasController extends Controller
|
||||||
@@ -13,14 +16,34 @@ class DeleteAliasController extends Controller
|
|||||||
$this->middleware('throttle:6,1');
|
$this->middleware('throttle:6,1');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deletePost($id)
|
public function deletePost(Request $request, $id)
|
||||||
{
|
{
|
||||||
$alias = Alias::findOrFail($id);
|
$alias = Alias::findOrFail($id);
|
||||||
|
|
||||||
|
$wasNotDeleted = is_null($alias->deleted_at);
|
||||||
|
|
||||||
$alias->delete();
|
$alias->delete();
|
||||||
|
|
||||||
Log::info('One-Click Unsubscribe deleted alias: '.$alias->email.' ID: '.$id);
|
Log::info('One-Click Unsubscribe deleted alias: '.$alias->email.' ID: '.$id);
|
||||||
|
|
||||||
|
if ($wasNotDeleted) {
|
||||||
|
$cacheKey = "unsubscribe-delete-notify:{$alias->id}";
|
||||||
|
|
||||||
|
if (! Cache::has($cacheKey)) {
|
||||||
|
Cache::put($cacheKey, true, now()->addHour());
|
||||||
|
|
||||||
|
$user = $alias->user;
|
||||||
|
$user->notify(
|
||||||
|
(new AliasDeletedByUnsubscribeNotification(
|
||||||
|
$alias->email,
|
||||||
|
$request->ip(),
|
||||||
|
$request->userAgent(),
|
||||||
|
now()->format('F j, g:i A (T)'),
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response('');
|
return response('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
class ShowFailedDeliveryController extends Controller
|
class ShowFailedDeliveryController extends Controller
|
||||||
@@ -13,27 +12,39 @@ class ShowFailedDeliveryController extends Controller
|
|||||||
// Validate search query
|
// Validate search query
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'search' => 'nullable|string|max:50|min:2',
|
'search' => 'nullable|string|max:50|min:2',
|
||||||
|
'filter' => 'nullable|string|in:all,inbound,outbound',
|
||||||
|
'page_size' => 'nullable|integer|in:25,50,100',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$failedDeliveries = user()
|
$query = user()
|
||||||
->failedDeliveries()
|
->failedDeliveries()
|
||||||
->with(['recipient:id,email', 'alias:id,email'])
|
->with(['recipient:id,email', 'alias:id,email'])
|
||||||
->select(['alias_id', 'email_type', 'code', 'attempted_at', 'created_at', 'id', 'user_id', 'recipient_id', 'remote_mta', 'sender', 'destination', 'is_stored', 'resent'])
|
->select(['alias_id', 'email_type', 'code', 'attempted_at', 'created_at', 'id', 'user_id', 'recipient_id', 'remote_mta', 'sender', 'destination', 'is_stored', 'resent'])
|
||||||
->latest()
|
->latest();
|
||||||
->get();
|
|
||||||
|
|
||||||
if (isset($validated['search'])) {
|
$filter = $validated['filter'] ?? 'all';
|
||||||
$searchTerm = strtolower($validated['search']);
|
|
||||||
|
|
||||||
$failedDeliveries = $failedDeliveries->filter(function ($failedDelivery) use ($searchTerm) {
|
if ($filter === 'inbound') {
|
||||||
return Str::contains(strtolower($failedDelivery->code), $searchTerm);
|
$query->where('email_type', 'IR');
|
||||||
})->values();
|
} elseif ($filter === 'outbound') {
|
||||||
|
$query->where('email_type', '!=', 'IR');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($validated['search'])) {
|
||||||
|
$query->where('code', 'like', '%'.$validated['search'].'%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$failedDeliveries = $query
|
||||||
|
->paginate($validated['page_size'] ?? 25)
|
||||||
|
->withQueryString()
|
||||||
|
->onEachSide(1);
|
||||||
|
|
||||||
return Inertia::render('FailedDeliveries', [
|
return Inertia::render('FailedDeliveries', [
|
||||||
'initialRows' => $failedDeliveries,
|
'initialRows' => fn () => $failedDeliveries,
|
||||||
'recipientOptions' => fn () => user()->verifiedRecipients()->select(['id', 'email'])->get(),
|
'recipientOptions' => fn () => user()->verifiedRecipients()->select(['id', 'email'])->get(),
|
||||||
'search' => $validated['search'] ?? null,
|
'search' => $validated['search'] ?? null,
|
||||||
|
'initialFilter' => $filter,
|
||||||
|
'initialPageSize' => isset($validated['page_size']) ? (int) $validated['page_size'] : 25,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
app/Http/Requests/IndexFailedDeliveryRequest.php
Normal file
42
app/Http/Requests/IndexFailedDeliveryRequest.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class IndexFailedDeliveryRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'page' => [
|
||||||
|
'nullable',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
'page.number' => [
|
||||||
|
'nullable',
|
||||||
|
'integer',
|
||||||
|
],
|
||||||
|
'page.size' => [
|
||||||
|
'nullable',
|
||||||
|
'integer',
|
||||||
|
'max:100',
|
||||||
|
'min:1',
|
||||||
|
],
|
||||||
|
'filter' => [
|
||||||
|
'nullable',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
'filter.email_type' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'in:inbound,outbound',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,9 +41,12 @@ class FailedDelivery extends Model
|
|||||||
'sender',
|
'sender',
|
||||||
'destination',
|
'destination',
|
||||||
'email_type',
|
'email_type',
|
||||||
|
'ir_dedupe_key',
|
||||||
'status',
|
'status',
|
||||||
'code',
|
'code',
|
||||||
'attempted_at',
|
'attempted_at',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -109,6 +112,9 @@ class FailedDelivery extends Model
|
|||||||
'RSL' => 'Reached Reply/Send Limit',
|
'RSL' => 'Reached Reply/Send Limit',
|
||||||
'SRSA' => 'Spam Reply/Send Attempt',
|
'SRSA' => 'Spam Reply/Send Attempt',
|
||||||
'AIF' => 'Aliases Import Finished',
|
'AIF' => 'Aliases Import Finished',
|
||||||
|
'IR' => 'Inbound Rejection',
|
||||||
|
'ADLN' => 'Alias Deleted by One-Click Unsubscribe',
|
||||||
|
'ADUN' => 'Alias Deactivated by One-Click Unsubscribe',
|
||||||
default => 'Forward',
|
default => 'Forward',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
|
class AliasDeactivatedByUnsubscribeNotification extends Notification implements ShouldBeEncrypted, ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected string $aliasEmail,
|
||||||
|
protected string $aliasId,
|
||||||
|
protected ?string $ipAddress = null,
|
||||||
|
protected ?string $userAgent = null,
|
||||||
|
protected ?string $timestamp = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function via($notifiable)
|
||||||
|
{
|
||||||
|
return ['mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return MailMessage
|
||||||
|
*/
|
||||||
|
public function toMail($notifiable)
|
||||||
|
{
|
||||||
|
$recipient = $notifiable->defaultRecipient;
|
||||||
|
$fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
|
||||||
|
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject('Alias deactivated via one-click unsubscribe')
|
||||||
|
->markdown('mail.alias_deactivated_by_unsubscribe', [
|
||||||
|
'aliasEmail' => $this->aliasEmail,
|
||||||
|
'aliasId' => $this->aliasId,
|
||||||
|
'ipAddress' => $this->ipAddress,
|
||||||
|
'userAgent' => $this->userAgent,
|
||||||
|
'timestamp' => $this->timestamp,
|
||||||
|
'userId' => $notifiable->id,
|
||||||
|
'recipientId' => $notifiable->default_recipient_id,
|
||||||
|
'emailType' => 'ADUN',
|
||||||
|
'fingerprint' => $fingerprint,
|
||||||
|
])
|
||||||
|
->withSymfonyMessage(function (Email $message) {
|
||||||
|
$message->getHeaders()
|
||||||
|
->addTextHeader('Feedback-ID', 'ADUN:anonaddy');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray($notifiable)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Notifications/AliasDeletedByUnsubscribeNotification.php
Normal file
67
app/Notifications/AliasDeletedByUnsubscribeNotification.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
|
class AliasDeletedByUnsubscribeNotification extends Notification implements ShouldBeEncrypted, ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected string $aliasEmail,
|
||||||
|
protected ?string $ipAddress = null,
|
||||||
|
protected ?string $userAgent = null,
|
||||||
|
protected ?string $timestamp = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function via($notifiable)
|
||||||
|
{
|
||||||
|
return ['mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return MailMessage
|
||||||
|
*/
|
||||||
|
public function toMail($notifiable)
|
||||||
|
{
|
||||||
|
$recipient = $notifiable->defaultRecipient;
|
||||||
|
$fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
|
||||||
|
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject('Alias deleted via one-click unsubscribe')
|
||||||
|
->markdown('mail.alias_deleted_by_unsubscribe', [
|
||||||
|
'aliasEmail' => $this->aliasEmail,
|
||||||
|
'ipAddress' => $this->ipAddress,
|
||||||
|
'userAgent' => $this->userAgent,
|
||||||
|
'timestamp' => $this->timestamp,
|
||||||
|
'userId' => $notifiable->id,
|
||||||
|
'recipientId' => $notifiable->default_recipient_id,
|
||||||
|
'emailType' => 'ADLN',
|
||||||
|
'fingerprint' => $fingerprint,
|
||||||
|
])
|
||||||
|
->withSymfonyMessage(function (Email $message) {
|
||||||
|
$message->getHeaders()
|
||||||
|
->addTextHeader('Feedback-ID', 'ADLN:anonaddy');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray($notifiable)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ use Illuminate\Contracts\Validation\ValidationRule;
|
|||||||
|
|
||||||
class VerifiedRecipientId implements ValidationRule
|
class VerifiedRecipientId implements ValidationRule
|
||||||
{
|
{
|
||||||
protected $verifiedRecipientIds;
|
protected ?array $verifiedRecipientIds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new rule instance.
|
* Create a new rule instance.
|
||||||
@@ -16,14 +16,7 @@ class VerifiedRecipientId implements ValidationRule
|
|||||||
*/
|
*/
|
||||||
public function __construct(?array $verifiedRecipientIds = null)
|
public function __construct(?array $verifiedRecipientIds = null)
|
||||||
{
|
{
|
||||||
if (! is_null($verifiedRecipientIds)) {
|
$this->verifiedRecipientIds = $verifiedRecipientIds;
|
||||||
$this->verifiedRecipientIds = $verifiedRecipientIds;
|
|
||||||
} else {
|
|
||||||
$this->verifiedRecipientIds = user()
|
|
||||||
->verifiedRecipients()
|
|
||||||
->pluck('id')
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +24,13 @@ class VerifiedRecipientId implements ValidationRule
|
|||||||
*/
|
*/
|
||||||
public function validate(string $attribute, mixed $ids, Closure $fail): void
|
public function validate(string $attribute, mixed $ids, Closure $fail): void
|
||||||
{
|
{
|
||||||
|
if (is_null($this->verifiedRecipientIds)) {
|
||||||
|
$this->verifiedRecipientIds = user()
|
||||||
|
->verifiedRecipients()
|
||||||
|
->pluck('id')
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
// Multiple calls to $fail simply add more validation errors, they don't stop processing.
|
// Multiple calls to $fail simply add more validation errors, they don't stop processing.
|
||||||
if (! is_array($ids)) {
|
if (! is_array($ids)) {
|
||||||
$fail('Invalid Recipient');
|
$fail('Invalid Recipient');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Services;
|
|||||||
|
|
||||||
use App\Models\Alias;
|
use App\Models\Alias;
|
||||||
use App\Models\EmailData;
|
use App\Models\EmailData;
|
||||||
|
use App\Models\Rule;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class UserRuleChecker
|
|||||||
protected function getRuleIdsAndActions(string $emailType): array
|
protected function getRuleIdsAndActions(string $emailType): array
|
||||||
{
|
{
|
||||||
$ruleIdsAndActions = [];
|
$ruleIdsAndActions = [];
|
||||||
|
$matchedRuleIds = [];
|
||||||
|
|
||||||
$method = "activeRulesFor{$emailType}Ordered";
|
$method = "activeRulesFor{$emailType}Ordered";
|
||||||
$rules = $this->user->{$method};
|
$rules = $this->user->{$method};
|
||||||
@@ -43,11 +45,14 @@ class UserRuleChecker
|
|||||||
if ($this->ruleConditionsSatisfied($rule->conditions, $rule->operator)) {
|
if ($this->ruleConditionsSatisfied($rule->conditions, $rule->operator)) {
|
||||||
$ruleIdsAndActions[$rule->id] = $rule->actions;
|
$ruleIdsAndActions[$rule->id] = $rule->actions;
|
||||||
|
|
||||||
// Increment applied count
|
$matchedRuleIds[] = $rule->id;
|
||||||
$rule->increment('applied', 1, ['last_applied' => now()]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! empty($matchedRuleIds)) {
|
||||||
|
Rule::whereIn('id', $matchedRuleIds)->increment('applied', 1, ['last_applied' => now()]);
|
||||||
|
}
|
||||||
|
|
||||||
return $ruleIdsAndActions;
|
return $ruleIdsAndActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
composer.lock
generated
36
composer.lock
generated
@@ -1689,16 +1689,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "inertiajs/inertia-laravel",
|
"name": "inertiajs/inertia-laravel",
|
||||||
"version": "v2.0.23",
|
"version": "v2.0.24",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/inertiajs/inertia-laravel.git",
|
"url": "https://github.com/inertiajs/inertia-laravel.git",
|
||||||
"reference": "20438dc5a0f3008965ccaa96e990d03116102d80"
|
"reference": "ea345adad12f110edbbc4bef03b69c2374a535d4"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/20438dc5a0f3008965ccaa96e990d03116102d80",
|
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/ea345adad12f110edbbc4bef03b69c2374a535d4",
|
||||||
"reference": "20438dc5a0f3008965ccaa96e990d03116102d80",
|
"reference": "ea345adad12f110edbbc4bef03b69c2374a535d4",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1756,9 +1756,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
|
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
|
||||||
"source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.23"
|
"source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.24"
|
||||||
},
|
},
|
||||||
"time": "2026-04-07T14:01:31+00:00"
|
"time": "2026-04-10T14:36:44+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "intervention/gif",
|
"name": "intervention/gif",
|
||||||
@@ -4178,16 +4178,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoffice/phpspreadsheet",
|
"name": "phpoffice/phpspreadsheet",
|
||||||
"version": "1.30.2",
|
"version": "1.30.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||||
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2"
|
"reference": "d28d4827f934469e7ca4de940ab0abd0788d1e65"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
|
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/d28d4827f934469e7ca4de940ab0abd0788d1e65",
|
||||||
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
|
"reference": "d28d4827f934469e7ca4de940ab0abd0788d1e65",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4280,9 +4280,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.2"
|
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.3"
|
||||||
},
|
},
|
||||||
"time": "2026-01-11T05:58:24+00:00"
|
"time": "2026-04-10T03:47:16+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
@@ -8861,16 +8861,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webmozart/assert",
|
"name": "webmozart/assert",
|
||||||
"version": "2.1.6",
|
"version": "2.2.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/webmozarts/assert.git",
|
"url": "https://github.com/webmozarts/assert.git",
|
||||||
"reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8"
|
"reference": "1b99650e7ffcad232624a260bc7fbdec2ffc407c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8",
|
"url": "https://api.github.com/repos/webmozarts/assert/zipball/1b99650e7ffcad232624a260bc7fbdec2ffc407c",
|
||||||
"reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8",
|
"reference": "1b99650e7ffcad232624a260bc7fbdec2ffc407c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -8917,9 +8917,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/webmozarts/assert/issues",
|
"issues": "https://github.com/webmozarts/assert/issues",
|
||||||
"source": "https://github.com/webmozarts/assert/tree/2.1.6"
|
"source": "https://github.com/webmozarts/assert/tree/2.2.0"
|
||||||
},
|
},
|
||||||
"time": "2026-02-27T10:28:38+00:00"
|
"time": "2026-04-09T16:54:47+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
|||||||
@@ -153,6 +153,17 @@ return [
|
|||||||
'secret' => env('BLOCKLIST_API_SECRET', ''),
|
'secret' => env('BLOCKLIST_API_SECRET', ''),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Postfix Log Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The path to the postfix log file, used for parsing inbound rejections
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postfix_log_path' => env('POSTFIX_LOG_PATH', '/var/log/mail.log'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| All Domains
|
| All Domains
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* Inbound rejection rows (email_type IR) use ir_dedupe_key (SHA-256 hex) for duplicate prevention.
|
||||||
|
* Outbound rows leave ir_dedupe_key null; MySQL/SQLite allow many nulls under a unique index.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('failed_deliveries', function (Blueprint $table) {
|
||||||
|
$table->string('ir_dedupe_key', 64)->nullable()->after('destination');
|
||||||
|
$table->unique('ir_dedupe_key', 'failed_deliveries_ir_dedupe_key_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('failed_deliveries', function (Blueprint $table) {
|
||||||
|
$table->dropUnique('failed_deliveries_ir_dedupe_key_unique');
|
||||||
|
$table->dropColumn('ir_dedupe_key');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('alias_recipients', function (Blueprint $table) {
|
||||||
|
$table->index('recipient_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('recipients', function (Blueprint $table) {
|
||||||
|
$table->index(['user_id', 'pending', 'created_at'], 'recipients_user_pending_created_at_index');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('alias_recipients', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('alias_recipients_recipient_id_index');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('recipients', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('recipients_user_pending_created_at_index');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
214
package-lock.json
generated
214
package-lock.json
generated
@@ -108,9 +108,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -291,9 +291,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.123.0",
|
"version": "0.124.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
|
||||||
"integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==",
|
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -311,9 +311,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==",
|
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -328,9 +328,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==",
|
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -345,9 +345,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==",
|
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -362,9 +362,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==",
|
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -379,9 +379,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==",
|
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -396,9 +396,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==",
|
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -413,9 +413,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==",
|
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -430,9 +430,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==",
|
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -447,9 +447,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==",
|
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -464,9 +464,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==",
|
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -481,9 +481,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==",
|
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -498,9 +498,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==",
|
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -515,9 +515,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==",
|
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -525,18 +525,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.9.1",
|
"@emnapi/core": "1.9.2",
|
||||||
"@emnapi/runtime": "1.9.1",
|
"@emnapi/runtime": "1.9.2",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.2"
|
"@napi-rs/wasm-runtime": "^1.1.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==",
|
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -551,9 +551,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==",
|
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -671,12 +671,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.5.2",
|
"version": "25.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||||
"integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
|
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
@@ -1166,9 +1166,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.14.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||||
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
|
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.15.11",
|
||||||
@@ -1177,9 +1177,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.16",
|
"version": "2.10.17",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz",
|
||||||
"integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
|
"integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.cjs"
|
"baseline-browser-mapping": "dist/cli.cjs"
|
||||||
@@ -3235,9 +3235,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.8.1",
|
"version": "3.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
|
||||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
"integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3260,9 +3260,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||||
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
@@ -3413,14 +3413,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==",
|
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.123.0",
|
"@oxc-project/types": "=0.124.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.13"
|
"@rolldown/pluginutils": "1.0.0-rc.15"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -3429,27 +3429,27 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.13",
|
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.13",
|
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.13",
|
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.13",
|
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13",
|
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13",
|
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.13",
|
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.13",
|
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.13",
|
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13"
|
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.13",
|
"version": "1.0.0-rc.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
||||||
"integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==",
|
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -3549,13 +3549,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/side-channel-list": {
|
"node_modules/side-channel-list": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"object-inspect": "^1.13.3"
|
"object-inspect": "^1.13.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -3984,9 +3984,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.18.2",
|
"version": "7.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
@@ -4026,9 +4026,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.7",
|
"version": "8.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
||||||
"integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==",
|
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -4036,7 +4036,7 @@
|
|||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
"rolldown": "1.0.0-rc.13",
|
"rolldown": "1.0.0-rc.15",
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.15"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4228,9 +4228,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.105.4",
|
"version": "5.106.1",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.1.tgz",
|
||||||
"integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
|
"integrity": "sha512-EW8af29ak8Oaf4T8k8YsajjrDBDYgnKZ5er6ljWFJsXABfTNowQfvHLftwcepVgdz+IoLSdEAbBiM9DFXoll9w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -117,6 +117,18 @@ try {
|
|||||||
|
|
||||||
$aliasHasSharedDomain = in_array($aliasDomain, $allDomains);
|
$aliasHasSharedDomain = in_array($aliasDomain, $allDomains);
|
||||||
|
|
||||||
|
// Check if the alias has a username subdomain
|
||||||
|
$matchedBaseDomain = null;
|
||||||
|
|
||||||
|
foreach ($allDomains as $domain) {
|
||||||
|
if (str_ends_with($aliasDomain, ".{$domain}")) {
|
||||||
|
$matchedBaseDomain = $domain;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliasHasUsernameDomain = ! is_null($matchedBaseDomain);
|
||||||
|
|
||||||
// Check if it is a bounce with a valid VERP...
|
// Check if it is a bounce with a valid VERP...
|
||||||
if (substr($aliasEmail, 0, 2) === 'b_') {
|
if (substr($aliasEmail, 0, 2) === 'b_') {
|
||||||
if ($outboundMessageId = getIdFromVerp($aliasLocalPart, $aliasEmail)) {
|
if ($outboundMessageId = getIdFromVerp($aliasLocalPart, $aliasEmail)) {
|
||||||
@@ -145,39 +157,30 @@ try {
|
|||||||
$aliasEmail = before($aliasEmail, '+').'@'.$aliasDomain;
|
$aliasEmail = before($aliasEmail, '+').'@'.$aliasDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$aliasActionQuery = Database::table('aliases')
|
||||||
|
->leftJoin('users', 'aliases.user_id', '=', 'users.id')
|
||||||
|
->where('aliases.email', $aliasEmail)
|
||||||
|
->selectRaw('CASE
|
||||||
|
WHEN aliases.deleted_at IS NOT NULL THEN ?
|
||||||
|
WHEN aliases.active = 0 THEN ?
|
||||||
|
WHEN users.reject_until > NOW() THEN ?
|
||||||
|
WHEN users.defer_until > NOW() THEN ?
|
||||||
|
ELSE "DUNNO"
|
||||||
|
END', [
|
||||||
|
ACTION_DOES_NOT_EXIST,
|
||||||
|
ACTION_ALIAS_DISCARD,
|
||||||
|
ACTION_REJECT,
|
||||||
|
ACTION_DEFER,
|
||||||
|
])
|
||||||
|
->first();
|
||||||
|
|
||||||
// Check if the alias already exists or not
|
// Check if the alias already exists or not
|
||||||
$noAliasExists = Database::table('aliases')->select('id')->where('email', $aliasEmail)->doesntExist();
|
$noAliasExists = is_null($aliasActionQuery);
|
||||||
|
|
||||||
if ($noAliasExists && $aliasHasSharedDomain) {
|
if ($noAliasExists && $aliasHasSharedDomain) {
|
||||||
// If admin username is set then allow through with catch-all
|
sendAction(ACTION_DOES_NOT_EXIST);
|
||||||
if ($adminUsername) {
|
|
||||||
sendAction('DUNNO');
|
|
||||||
} else {
|
|
||||||
sendAction(ACTION_DOES_NOT_EXIST);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$aliasAction = null;
|
$aliasAction = $noAliasExists ? null : getAction($aliasActionQuery);
|
||||||
|
|
||||||
if (! $noAliasExists) {
|
|
||||||
$aliasActionQuery = Database::table('aliases')
|
|
||||||
->leftJoin('users', 'aliases.user_id', '=', 'users.id')
|
|
||||||
->where('aliases.email', $aliasEmail)
|
|
||||||
->selectRaw('CASE
|
|
||||||
WHEN aliases.deleted_at IS NOT NULL THEN ?
|
|
||||||
WHEN aliases.active = 0 THEN ?
|
|
||||||
WHEN users.reject_until > NOW() THEN ?
|
|
||||||
WHEN users.defer_until > NOW() THEN ?
|
|
||||||
ELSE "DUNNO"
|
|
||||||
END', [
|
|
||||||
ACTION_DOES_NOT_EXIST,
|
|
||||||
ACTION_ALIAS_DISCARD,
|
|
||||||
ACTION_REJECT,
|
|
||||||
ACTION_DEFER,
|
|
||||||
])
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$aliasAction = getAction($aliasActionQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($aliasAction, [ACTION_ALIAS_DISCARD, ACTION_DOES_NOT_EXIST])) {
|
if (in_array($aliasAction, [ACTION_ALIAS_DISCARD, ACTION_DOES_NOT_EXIST])) {
|
||||||
// If the alias is inactive or deleted then increment the blocked count
|
// If the alias is inactive or deleted then increment the blocked count
|
||||||
@@ -191,17 +194,11 @@ try {
|
|||||||
|
|
||||||
sendAction($aliasAction);
|
sendAction($aliasAction);
|
||||||
} elseif ($aliasHasUsernameDomain) {
|
} elseif ($aliasHasUsernameDomain) {
|
||||||
$concatDomainsStatement = array_reduce(array_keys($allDomains), function ($carry, $key) {
|
$aliasUsername = substr($aliasDomain, 0, -(strlen($matchedBaseDomain) + 1));
|
||||||
$comma = $key === 0 ? '' : ',';
|
|
||||||
|
|
||||||
return "{$carry}{$comma}CONCAT(usernames.username, ?)";
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
$dotDomains = array_map(fn ($domain) => ".{$domain}", $allDomains);
|
|
||||||
|
|
||||||
$usernameActionQuery = Database::table('usernames')
|
$usernameActionQuery = Database::table('usernames')
|
||||||
->leftJoin('users', 'usernames.user_id', '=', 'users.id')
|
->leftJoin('users', 'usernames.user_id', '=', 'users.id')
|
||||||
->whereRaw('? IN ('.$concatDomainsStatement.')', [$aliasDomain, ...$dotDomains])
|
->where('usernames.username', $aliasUsername)
|
||||||
->selectRaw('CASE
|
->selectRaw('CASE
|
||||||
WHEN ? AND usernames.catch_all = 0 AND (usernames.auto_create_regex IS NULL OR ? NOT REGEXP usernames.auto_create_regex) THEN ?
|
WHEN ? AND usernames.catch_all = 0 AND (usernames.auto_create_regex IS NULL OR ? NOT REGEXP usernames.auto_create_regex) THEN ?
|
||||||
WHEN usernames.active = 0 THEN ?
|
WHEN usernames.active = 0 THEN ?
|
||||||
@@ -322,22 +319,6 @@ function before($subject, $search)
|
|||||||
return $result === false ? $subject : $result;
|
return $result === false ? $subject : $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the portion of a string before the last occurrence of a given value.
|
|
||||||
function beforeLast($subject, $search)
|
|
||||||
{
|
|
||||||
if ($search === '') {
|
|
||||||
return $subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pos = mb_strrpos($subject, $search);
|
|
||||||
|
|
||||||
if ($pos === false) {
|
|
||||||
return $subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mb_substr($subject, 0, $pos, 'UTF-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if a given string ends with a given substring
|
// Determine if a given string ends with a given substring
|
||||||
function endsWith($haystack, $needles)
|
function endsWith($haystack, $needles)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -446,7 +446,7 @@
|
|||||||
<icon name="pin" class="inline-block w-4 h-4 fill-current" />
|
<icon name="pin" class="inline-block w-4 h-4 fill-current" />
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="text-grey-400 tooltip outline-none"
|
class="text-grey-400 tooltip outline-none text-left"
|
||||||
data-tippy-content="Click to copy"
|
data-tippy-content="Click to copy"
|
||||||
@click="clipboard(getAliasEmail(rows[props.row.originalIndex]))"
|
@click="clipboard(getAliasEmail(rows[props.row.originalIndex]))"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,16 +17,25 @@
|
|||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||||
|
<select
|
||||||
|
v-model="filterType"
|
||||||
|
@change="updateFilter"
|
||||||
|
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-grey-300 focus:outline-none focus:ring-cyan-500 focus:border-cyan-500 sm:text-sm rounded-md dark:border-grey-600 dark:bg-grey-700 dark:text-grey-200"
|
||||||
|
>
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="outbound">Outbound Bounces</option>
|
||||||
|
<option value="inbound">Inbound Rejections</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<vue-good-table
|
<vue-good-table
|
||||||
v-if="rows.length"
|
v-if="rows.length"
|
||||||
v-on:sort-change="debounceToolips"
|
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:sort-options="{
|
:sort-options="{
|
||||||
enabled: true,
|
enabled: false,
|
||||||
initialSortBy: { field: 'created_at', type: 'desc' },
|
|
||||||
}"
|
}"
|
||||||
styleClass="vgt-table"
|
styleClass="vgt-table"
|
||||||
>
|
>
|
||||||
@@ -138,13 +147,158 @@
|
|||||||
</template>
|
</template>
|
||||||
</vue-good-table>
|
</vue-good-table>
|
||||||
|
|
||||||
<div v-else-if="search" class="text-center">
|
<!-- Pagination -->
|
||||||
|
<div
|
||||||
|
v-if="$page.props.initialRows.data.length"
|
||||||
|
class="mt-4 rounded-lg shadow flex items-center justify-between bg-white px-4 py-3 sm:px-6 overflow-x-auto horizontal-scroll dark:bg-grey-900"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 justify-between items-center md:hidden gap-x-3">
|
||||||
|
<Link
|
||||||
|
v-if="$page.props.initialRows.prev_page_url"
|
||||||
|
:href="$page.props.initialRows.prev_page_url"
|
||||||
|
as="button"
|
||||||
|
class="relative inline-flex items-center rounded-md border border-grey-300 bg-white px-4 py-2 text-sm font-medium text-grey-700 hover:bg-grey-50 dark:bg-grey-950 dark:hover:bg-grey-900 dark:text-grey-200"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Link>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="relative inline-flex h-min items-center rounded-md border border-grey-300 px-4 py-2 text-sm font-medium text-grey-700 bg-grey-100 dark:bg-grey-800 dark:text-grey-200"
|
||||||
|
>Previous</span
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-y-2">
|
||||||
|
<p class="text-sm text-grey-700 text-center dark:text-grey-200">
|
||||||
|
Showing
|
||||||
|
{{ ' ' }}
|
||||||
|
<span class="font-medium">{{ $page.props.initialRows.from.toLocaleString() }}</span>
|
||||||
|
{{ ' ' }}
|
||||||
|
to
|
||||||
|
{{ ' ' }}
|
||||||
|
<span class="font-medium">{{ $page.props.initialRows.to.toLocaleString() }}</span>
|
||||||
|
{{ ' ' }}
|
||||||
|
of
|
||||||
|
{{ ' ' }}
|
||||||
|
<span class="font-medium">{{ $page.props.initialRows.total.toLocaleString() }}</span>
|
||||||
|
{{ ' ' }}
|
||||||
|
{{ $page.props.initialRows.total === 1 ? 'result' : 'results' }}
|
||||||
|
</p>
|
||||||
|
<select
|
||||||
|
v-model.number="pageSize"
|
||||||
|
@change="updatePageSize"
|
||||||
|
:disabled="updatePageSizeLoading"
|
||||||
|
class="relative rounded border-0 bg-transparent py-1 pr-8 text-grey-900 text-sm ring-1 ring-inset focus:z-10 focus:ring-2 focus:ring-inset ring-grey-300 focus:ring-indigo-600 disabled:cursor-not-allowed dark:text-grey-200"
|
||||||
|
>
|
||||||
|
<option class="dark:bg-grey-900" v-for="size in pageSizeOptions" :value="size">
|
||||||
|
{{ size }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
v-if="$page.props.initialRows.next_page_url"
|
||||||
|
:href="$page.props.initialRows.next_page_url"
|
||||||
|
as="button"
|
||||||
|
class="relative inline-flex h-min items-center rounded-md border border-grey-300 bg-white px-4 py-2 text-sm font-medium text-grey-700 dark:bg-grey-950 dark:hover:bg-grey-900 dark:text-grey-200 hover:bg-grey-50"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Link>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="relative inline-flex items-center rounded-md border border-grey-300 px-4 py-2 text-sm font-medium text-grey-700 dark:text-grey-200 bg-grey-100 dark:bg-grey-800"
|
||||||
|
>Next</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:flex md:flex-1 md:items-center md:justify-between md:gap-x-2">
|
||||||
|
<div class="flex items-center gap-x-2">
|
||||||
|
<p class="text-sm text-grey-700 dark:text-grey-200">
|
||||||
|
Showing
|
||||||
|
{{ ' ' }}
|
||||||
|
<span class="font-medium">{{ $page.props.initialRows.from.toLocaleString() }}</span>
|
||||||
|
{{ ' ' }}
|
||||||
|
to
|
||||||
|
{{ ' ' }}
|
||||||
|
<span class="font-medium">{{ $page.props.initialRows.to.toLocaleString() }}</span>
|
||||||
|
{{ ' ' }}
|
||||||
|
of
|
||||||
|
{{ ' ' }}
|
||||||
|
<span class="font-medium">{{ $page.props.initialRows.total.toLocaleString() }}</span>
|
||||||
|
{{ ' ' }}
|
||||||
|
{{ $page.props.initialRows.total === 1 ? 'result' : 'results' }}
|
||||||
|
</p>
|
||||||
|
<select
|
||||||
|
v-model.number="pageSize"
|
||||||
|
@change="updatePageSize"
|
||||||
|
:disabled="updatePageSizeLoading"
|
||||||
|
class="relative rounded border-0 bg-transparent py-1 pr-8 text-grey-900 text-sm ring-1 ring-inset focus:z-10 focus:ring-2 focus:ring-inset ring-grey-300 focus:ring-indigo-600 disabled:cursor-not-allowed dark:text-grey-200"
|
||||||
|
>
|
||||||
|
<option class="dark:bg-grey-900" v-for="size in pageSizeOptions" :value="size">
|
||||||
|
{{ size }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||||
|
<Link
|
||||||
|
v-if="$page.props.initialRows.prev_page_url"
|
||||||
|
:href="$page.props.initialRows.prev_page_url"
|
||||||
|
class="relative inline-flex items-center rounded-l-md border border-grey-300 bg-white px-2 py-2 text-sm font-medium text-grey-500 hover:bg-grey-50 focus:z-20 dark:bg-grey-900 dark:hover:bg-grey-950 dark:border-grey-500"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Previous</span>
|
||||||
|
<ChevronLeftIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="disabled cursor-not-allowed relative inline-flex items-center rounded-l-md border border-grey-300 bg-white px-2 py-2 text-sm font-medium text-grey-500 focus:z-20 dark:bg-grey-800 dark:border-grey-500"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Previous</span>
|
||||||
|
<ChevronLeftIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div v-for="link in links" v-bind:key="link.label">
|
||||||
|
<Link
|
||||||
|
v-if="link.url"
|
||||||
|
:href="link.url"
|
||||||
|
aria-current="page"
|
||||||
|
class="relative inline-flex items-center border z-10 px-4 py-2 text-sm font-medium focus:z-20"
|
||||||
|
:class="
|
||||||
|
link.active
|
||||||
|
? 'border-indigo-500 bg-indigo-50 text-indigo-600 dark:bg-grey-950 dark:text-grey-100 dark:border-grey-500'
|
||||||
|
: 'border-grey-300 bg-white text-grey-500 hover:bg-grey-50 dark:bg-grey-900 dark:hover:bg-grey-950 dark:text-grey-200 dark:border-grey-500'
|
||||||
|
"
|
||||||
|
>{{ link.label }}</Link
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="relative inline-flex items-center border border-grey-300 bg-white px-4 py-2 text-sm font-medium text-grey-700 dark:bg-grey-900 dark:text-grey-200 dark:border-grey-500"
|
||||||
|
>...</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
v-if="$page.props.initialRows.next_page_url"
|
||||||
|
:href="$page.props.initialRows.next_page_url"
|
||||||
|
class="relative inline-flex items-center rounded-r-md border border-grey-300 bg-white px-2 py-2 text-sm font-medium text-grey-500 hover:bg-grey-50 focus:z-20 dark:bg-grey-900 dark:hover:bg-grey-950 dark:text-grey-200 dark:border-grey-500"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Next</span>
|
||||||
|
<ChevronRightIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="disabled cursor-not-allowed relative inline-flex items-center rounded-r-md border border-grey-300 bg-white px-2 py-2 text-sm font-medium text-grey-500 focus:z-20 dark:bg-grey-800 dark:text-grey-200 dark:border-grey-500"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Next</span>
|
||||||
|
<ChevronRightIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="search || filterType !== 'all'" class="text-center">
|
||||||
<ExclamationTriangleIcon class="mx-auto h-16 w-16 text-grey-400 dark:text-grey-200" />
|
<ExclamationTriangleIcon class="mx-auto h-16 w-16 text-grey-400 dark:text-grey-200" />
|
||||||
<h3 class="mt-2 text-lg font-medium text-grey-900 dark:text-white">
|
<h3 class="mt-2 text-lg font-medium text-grey-900 dark:text-white">
|
||||||
No Failed Deliveries found for that search
|
No Failed Deliveries found for that search or filter
|
||||||
</h3>
|
</h3>
|
||||||
<p class="mt-1 text-md text-grey-500 dark:text-grey-200">
|
<p class="mt-1 text-md text-grey-500 dark:text-grey-200">
|
||||||
Try entering a different search term.
|
Try entering a different search term or changing the filter.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<Link
|
<Link
|
||||||
@@ -195,7 +349,7 @@
|
|||||||
<div class="mt-6 flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
|
<div class="mt-6 flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="resendFailedDelivery(failedDeliveryToResend.id)"
|
@click="resendFailedDelivery(failedDeliveryToResend)"
|
||||||
class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
|
class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
|
||||||
:disabled="resendFailedDeliveryLoading"
|
:disabled="resendFailedDeliveryLoading"
|
||||||
>
|
>
|
||||||
@@ -252,12 +406,12 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="mt-4 text-grey-700 dark:text-grey-200">
|
<p class="mt-4 text-grey-700 dark:text-grey-200">
|
||||||
This page allows you to see any failed deliveries relating to your account and the reason
|
This page allows you to see any failed deliveries relating to your account and the reason
|
||||||
why they failed.
|
why they failed. It also displays any inbound emails that were rejected by the addy.io
|
||||||
|
servers before reaching your alias.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-4 text-grey-700 dark:text-grey-200">
|
<p class="mt-4 text-grey-700 dark:text-grey-200">
|
||||||
Only failed delivery attempts from the addy.io servers to your recipients (or reply/send
|
Only failed delivery attempts from the addy.io servers to your recipients (or reply/send
|
||||||
attempts from your aliases) will be shown here. It will not show messages that failed to
|
attempts from your aliases) and inbound rejections to your aliases will be shown here.
|
||||||
reach the addy.io server from some other sender.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-6 flex flex-col sm:flex-row">
|
<div class="mt-6 flex flex-col sm:flex-row">
|
||||||
@@ -316,19 +470,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
import { Head, Link } from '@inertiajs/vue3'
|
import { Head, Link, router } from '@inertiajs/vue3'
|
||||||
import Modal from '../Components/Modal.vue'
|
import Modal from '../Components/Modal.vue'
|
||||||
import { roundArrow } from 'tippy.js'
|
import { roundArrow } from 'tippy.js'
|
||||||
import tippy from 'tippy.js'
|
import tippy from 'tippy.js'
|
||||||
import { notify } from '@kyvg/vue3-notification'
|
import { notify } from '@kyvg/vue3-notification'
|
||||||
import { VueGoodTable } from 'vue-good-table-next'
|
import { VueGoodTable } from 'vue-good-table-next'
|
||||||
import Multiselect from '@vueform/multiselect'
|
import Multiselect from '@vueform/multiselect'
|
||||||
import { InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/vue/24/outline'
|
import {
|
||||||
|
InformationCircleIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from '@heroicons/vue/24/outline'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
initialRows: {
|
initialRows: {
|
||||||
type: Array,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
recipientOptions: {
|
recipientOptions: {
|
||||||
@@ -338,13 +497,27 @@ const props = defineProps({
|
|||||||
search: {
|
search: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
initialFilter: {
|
||||||
|
type: String,
|
||||||
|
default: 'all',
|
||||||
|
},
|
||||||
|
initialPageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 25,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
addTooltips()
|
addTooltips()
|
||||||
})
|
})
|
||||||
|
|
||||||
const rows = ref(props.initialRows)
|
const rows = ref(props.initialRows.data)
|
||||||
|
const links = ref(props.initialRows.links.slice(1, -1))
|
||||||
|
|
||||||
|
const filterType = ref(props.initialFilter)
|
||||||
|
const pageSize = ref(props.initialPageSize)
|
||||||
|
const updatePageSizeLoading = ref(false)
|
||||||
|
const pageSizeOptions = [25, 50, 100]
|
||||||
|
|
||||||
const resendFailedDeliveryLoading = ref(false)
|
const resendFailedDeliveryLoading = ref(false)
|
||||||
const resendFailedDeliveryModalOpen = ref(false)
|
const resendFailedDeliveryModalOpen = ref(false)
|
||||||
@@ -362,6 +535,14 @@ const failedDeliveryIdToDelete = ref(null)
|
|||||||
const tippyInstance = ref(null)
|
const tippyInstance = ref(null)
|
||||||
const errors = ref({})
|
const errors = ref({})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialRows,
|
||||||
|
newVal => {
|
||||||
|
rows.value = newVal.data
|
||||||
|
links.value = newVal.links.slice(1, -1)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
label: 'Created',
|
label: 'Created',
|
||||||
@@ -408,12 +589,38 @@ const columns = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const resendFailedDelivery = id => {
|
const visitWithParams = (extraParams = {}, omitKeys = []) => {
|
||||||
|
let params = Object.assign({}, route().params, extraParams)
|
||||||
|
|
||||||
|
if (filterType.value === 'all') {
|
||||||
|
omitKeys.push('filter')
|
||||||
|
}
|
||||||
|
if (pageSize.value === 25) {
|
||||||
|
omitKeys.push('page_size')
|
||||||
|
}
|
||||||
|
|
||||||
|
router.visit(route('failed_deliveries.index', _.omit(params, omitKeys)), {
|
||||||
|
only: ['initialRows', 'search', 'initialFilter', 'initialPageSize'],
|
||||||
|
preserveState: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFilter = () => {
|
||||||
|
visitWithParams({ filter: filterType.value }, ['page'])
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePageSize = () => {
|
||||||
|
updatePageSizeLoading.value = true
|
||||||
|
visitWithParams({ page_size: pageSize.value }, ['page'])
|
||||||
|
updatePageSizeLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const resendFailedDelivery = failedDelivery => {
|
||||||
resendFailedDeliveryLoading.value = true
|
resendFailedDeliveryLoading.value = true
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(
|
.post(
|
||||||
`/api/v1/failed-deliveries/${id}/resend`,
|
`/api/v1/failed-deliveries/${failedDelivery.id}/resend`,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
recipient_ids: failedDeliveryRecipientsToResend.value,
|
recipient_ids: failedDeliveryRecipientsToResend.value,
|
||||||
}),
|
}),
|
||||||
@@ -422,6 +629,8 @@ const resendFailedDelivery = id => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
successMessage('Failed Delivery Resent Successfully')
|
||||||
|
failedDelivery.resent = true
|
||||||
resendFailedDeliveryModalOpen.value = false
|
resendFailedDeliveryModalOpen.value = false
|
||||||
resendFailedDeliveryLoading.value = false
|
resendFailedDeliveryLoading.value = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
@component('mail::message')
|
||||||
|
|
||||||
|
# Alias Deactivated via One-Click Unsubscribe
|
||||||
|
|
||||||
|
Your alias **{{ $aliasEmail }}** has been deactivated because a one-click unsubscribe request was received.
|
||||||
|
|
||||||
|
**Important:** This means all emails to this alias will now be discarded, not just emails from the sender in the email that the unsubscribe link was clicked.
|
||||||
|
|
||||||
|
@if($ipAddress || $userAgent || $timestamp)
|
||||||
|
**Request details:**
|
||||||
|
@if($timestamp)
|
||||||
|
- **When:** {{ $timestamp }}
|
||||||
|
@endif
|
||||||
|
@if($ipAddress)
|
||||||
|
- **IP:** {{ $ipAddress }}
|
||||||
|
@endif
|
||||||
|
@if($userAgent)
|
||||||
|
- **User Agent:** {{ $userAgent }}
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
If you did not intend to deactivate this alias, you can click the button below, search for it and reactivate it.
|
||||||
|
|
||||||
|
@component('mail::button', ['url' => config('app.url').'/aliases'])
|
||||||
|
View Your Aliases
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
@endcomponent
|
||||||
28
resources/views/mail/alias_deleted_by_unsubscribe.blade.php
Normal file
28
resources/views/mail/alias_deleted_by_unsubscribe.blade.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@component('mail::message')
|
||||||
|
|
||||||
|
# Alias Deleted via One-Click Unsubscribe
|
||||||
|
|
||||||
|
Your alias **{{ $aliasEmail }}** has been deleted because a one-click unsubscribe request was received.
|
||||||
|
|
||||||
|
**Important:** This means all emails to this alias will now be rejected, not just emails from the sender in the email that the unsubscribe link was clicked.
|
||||||
|
|
||||||
|
@if($ipAddress || $userAgent || $timestamp)
|
||||||
|
**Request details:**
|
||||||
|
@if($timestamp)
|
||||||
|
- **When:** {{ $timestamp }}
|
||||||
|
@endif
|
||||||
|
@if($ipAddress)
|
||||||
|
- **IP:** {{ $ipAddress }}
|
||||||
|
@endif
|
||||||
|
@if($userAgent)
|
||||||
|
- **User Agent:** {{ $userAgent }}
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
If you did not intend to delete this alias, you can click the button below, search for it and restore it.
|
||||||
|
|
||||||
|
@component('mail::button', ['url' => config('app.url').'/aliases?deleted=only'])
|
||||||
|
View Your Deleted Aliases
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
@endcomponent
|
||||||
@@ -19,6 +19,7 @@ Schedule::command('anonaddy:check-domains-mx-validation')->daily();
|
|||||||
Schedule::command('anonaddy:clear-failed-deliveries')->daily();
|
Schedule::command('anonaddy:clear-failed-deliveries')->daily();
|
||||||
Schedule::command('anonaddy:clear-outbound-messages')->everySixHours();
|
Schedule::command('anonaddy:clear-outbound-messages')->everySixHours();
|
||||||
Schedule::command('anonaddy:email-users-with-token-expiring-soon')->daily();
|
Schedule::command('anonaddy:email-users-with-token-expiring-soon')->daily();
|
||||||
|
Schedule::command('anonaddy:parse-postfix-mail-log')->everyFiveMinutes();
|
||||||
Schedule::command('auth:clear-resets')->daily();
|
Schedule::command('auth:clear-resets')->daily();
|
||||||
Schedule::command('sanctum:prune-expired --hours=168')->daily();
|
Schedule::command('sanctum:prune-expired --hours=168')->daily();
|
||||||
Schedule::command('cache:prune-stale-tags')->hourly();
|
Schedule::command('cache:prune-stale-tags')->hourly();
|
||||||
|
|||||||
@@ -52,6 +52,56 @@ class FailedDeliveriesTest extends TestCase
|
|||||||
$this->assertEquals($failedDelivery->code, $response->json()['data']['code']);
|
$this->assertEquals($failedDelivery->code, $response->json()['data']['code']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function user_can_filter_failed_deliveries_by_inbound_type()
|
||||||
|
{
|
||||||
|
FailedDelivery::factory()->count(2)->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => 'IR',
|
||||||
|
]);
|
||||||
|
FailedDelivery::factory()->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => 'F',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->json('GET', '/api/v1/failed-deliveries?filter[email_type]=inbound');
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$this->assertCount(2, $response->json()['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function user_can_filter_failed_deliveries_by_outbound_type()
|
||||||
|
{
|
||||||
|
FailedDelivery::factory()->count(2)->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => 'IR',
|
||||||
|
]);
|
||||||
|
FailedDelivery::factory()->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => 'F',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->json('GET', '/api/v1/failed-deliveries?filter[email_type]=outbound');
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$this->assertCount(1, $response->json()['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function user_can_paginate_failed_deliveries()
|
||||||
|
{
|
||||||
|
FailedDelivery::factory()->count(3)->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->json('GET', '/api/v1/failed-deliveries?page[size]=2&page[number]=1');
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$this->assertCount(2, $response->json()['data']);
|
||||||
|
$this->assertEquals(3, $response->json()['meta']['total']);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function user_can_delete_failed_delivery()
|
public function user_can_delete_failed_delivery()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Api;
|
namespace Tests\Feature\Api;
|
||||||
|
|
||||||
|
use App\Models\Alias;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\Recipient;
|
use App\Models\Recipient;
|
||||||
use App\Notifications\CustomVerifyEmail;
|
use App\Notifications\CustomVerifyEmail;
|
||||||
@@ -66,6 +67,24 @@ class RecipientsTest extends TestCase
|
|||||||
$this->assertArrayNotHasKey('aliases_count', $response->json()['data'][0]);
|
$this->assertArrayNotHasKey('aliases_count', $response->json()['data'][0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function recipients_index_alias_count_excludes_soft_deleted_aliases(): void
|
||||||
|
{
|
||||||
|
$recipient = Recipient::factory()->create(['user_id' => $this->user->id]);
|
||||||
|
$activeAlias = Alias::factory()->create(['user_id' => $this->user->id]);
|
||||||
|
$deletedAlias = Alias::factory()->create(['user_id' => $this->user->id]);
|
||||||
|
$activeAlias->recipients()->attach($recipient->id);
|
||||||
|
$deletedAlias->recipients()->attach($recipient->id);
|
||||||
|
$deletedAlias->delete();
|
||||||
|
|
||||||
|
$response = $this->json('GET', '/api/v1/recipients');
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$row = collect($response->json('data'))->firstWhere('id', $recipient->id);
|
||||||
|
$this->assertNotNull($row);
|
||||||
|
$this->assertSame(1, (int) $row['aliases_count']);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function recipient_show_omits_alias_count_when_filter_alias_count_is_false()
|
public function recipient_show_omits_alias_count_when_filter_alias_count_is_false()
|
||||||
{
|
{
|
||||||
|
|||||||
119
tests/Feature/DeactivateAliasOneClickTest.php
Normal file
119
tests/Feature/DeactivateAliasOneClickTest.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Alias;
|
||||||
|
use App\Notifications\AliasDeactivatedByUnsubscribeNotification;
|
||||||
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||||
|
use Illuminate\Routing\Middleware\ThrottleRequestsWithRedis;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DeactivateAliasOneClickTest extends TestCase
|
||||||
|
{
|
||||||
|
use LazilyRefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function signed_post_deactivates_alias_and_sends_notification()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = $this->createUser('johndoe');
|
||||||
|
|
||||||
|
$alias = Alias::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = URL::signedRoute('deactivate_post', ['alias' => $alias->id]);
|
||||||
|
|
||||||
|
$response = $this->post($url);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$this->assertFalse($alias->refresh()->active);
|
||||||
|
|
||||||
|
Notification::assertSentTo($user, AliasDeactivatedByUnsubscribeNotification::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function already_inactive_alias_does_not_send_notification()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = $this->createUser('johndoe');
|
||||||
|
|
||||||
|
$alias = Alias::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'active' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = URL::signedRoute('deactivate_post', ['alias' => $alias->id]);
|
||||||
|
|
||||||
|
$response = $this->post($url);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$this->assertFalse($alias->refresh()->active);
|
||||||
|
|
||||||
|
Notification::assertNotSentTo($user, AliasDeactivatedByUnsubscribeNotification::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function duplicate_requests_only_send_one_notification()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = $this->createUser('johndoe');
|
||||||
|
|
||||||
|
$alias = Alias::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = URL::signedRoute('deactivate_post', ['alias' => $alias->id]);
|
||||||
|
|
||||||
|
$this->post($url);
|
||||||
|
$this->assertFalse($alias->refresh()->active);
|
||||||
|
|
||||||
|
$alias->update(['active' => true]);
|
||||||
|
$this->post($url);
|
||||||
|
|
||||||
|
Notification::assertSentToTimes($user, AliasDeactivatedByUnsubscribeNotification::class, 1);
|
||||||
|
$this->assertTrue(Cache::has("unsubscribe-deactivate-notify:{$alias->id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function invalid_signature_returns_403()
|
||||||
|
{
|
||||||
|
$user = $this->createUser('johndoe');
|
||||||
|
|
||||||
|
$alias = Alias::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->post(route('deactivate_post', ['alias' => $alias->id]));
|
||||||
|
|
||||||
|
$response->assertStatus(403);
|
||||||
|
$this->assertTrue($alias->refresh()->active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function non_existent_alias_returns_404()
|
||||||
|
{
|
||||||
|
$url = URL::signedRoute('deactivate_post', ['alias' => '00000000-0000-0000-0000-000000000000']);
|
||||||
|
|
||||||
|
$response = $this->post($url);
|
||||||
|
|
||||||
|
$response->assertStatus(404);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,11 @@
|
|||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\Alias;
|
use App\Models\Alias;
|
||||||
|
use App\Notifications\AliasDeletedByUnsubscribeNotification;
|
||||||
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||||
use Illuminate\Routing\Middleware\ThrottleRequestsWithRedis;
|
use Illuminate\Routing\Middleware\ThrottleRequestsWithRedis;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
use Illuminate\Support\Facades\URL;
|
use Illuminate\Support\Facades\URL;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
@@ -22,8 +25,10 @@ class DeleteAliasOneClickTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function signed_post_deletes_alias_and_returns_empty_response()
|
public function signed_post_deletes_alias_and_sends_notification()
|
||||||
{
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
$user = $this->createUser('johndoe');
|
$user = $this->createUser('johndoe');
|
||||||
|
|
||||||
$alias = Alias::factory()->create([
|
$alias = Alias::factory()->create([
|
||||||
@@ -42,6 +47,53 @@ class DeleteAliasOneClickTest extends TestCase
|
|||||||
|
|
||||||
$alias->refresh();
|
$alias->refresh();
|
||||||
$this->assertNotNull($alias->deleted_at);
|
$this->assertNotNull($alias->deleted_at);
|
||||||
|
|
||||||
|
Notification::assertSentTo($user, AliasDeletedByUnsubscribeNotification::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function already_deleted_alias_returns_404_and_does_not_send_notification()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = $this->createUser('johndoe');
|
||||||
|
|
||||||
|
$alias = Alias::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'deleted_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = URL::signedRoute('delete_post', ['alias' => $alias->id]);
|
||||||
|
|
||||||
|
$response = $this->post($url);
|
||||||
|
|
||||||
|
$response->assertStatus(404);
|
||||||
|
|
||||||
|
Notification::assertNotSentTo($user, AliasDeletedByUnsubscribeNotification::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function duplicate_requests_only_send_one_notification()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = $this->createUser('johndoe');
|
||||||
|
|
||||||
|
$alias = Alias::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = URL::signedRoute('delete_post', ['alias' => $alias->id]);
|
||||||
|
|
||||||
|
$this->post($url);
|
||||||
|
$this->assertNotNull($alias->refresh()->deleted_at);
|
||||||
|
|
||||||
|
$alias->restore();
|
||||||
|
$this->post($url);
|
||||||
|
|
||||||
|
Notification::assertSentToTimes($user, AliasDeletedByUnsubscribeNotification::class, 1);
|
||||||
|
$this->assertTrue(Cache::has("unsubscribe-delete-notify:{$alias->id}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
|
|||||||
160
tests/Feature/ParsePostfixMailLogTest.php
Normal file
160
tests/Feature/ParsePostfixMailLogTest.php
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Alias;
|
||||||
|
use App\Models\FailedDelivery;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ParsePostfixMailLogTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected $logPath;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->user = $this->createUser();
|
||||||
|
|
||||||
|
Storage::fake('local');
|
||||||
|
$this->logPath = storage_path('app/mail.log');
|
||||||
|
Config::set('anonaddy.postfix_log_path', $this->logPath);
|
||||||
|
Config::set('anonaddy.all_domains', ['anonaddy.me']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
if (file_exists($this->logPath)) {
|
||||||
|
unlink($this->logPath);
|
||||||
|
}
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_parses_rejection_lines_and_creates_failed_delivery_for_users()
|
||||||
|
{
|
||||||
|
$alias = Alias::factory()->create(['user_id' => $this->user->id, 'email' => 'test@anonaddy.me']);
|
||||||
|
|
||||||
|
$logContent = "Mar 17 10:30:00 server postfix/smtpd[12345]: NOQUEUE: reject: RCPT from unknown[1.2.3.4]: 450 4.7.1 Client host rejected: cannot find your hostname; from=<s@x.com> to=<test@anonaddy.me> proto=ESMTP helo=<1.2.3.4>\n";
|
||||||
|
file_put_contents($this->logPath, $logContent);
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('failed_deliveries', [
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'alias_id' => $alias->id,
|
||||||
|
'email_type' => 'IR',
|
||||||
|
'code' => '450 4.7.1 Client host rejected: cannot find your hostname',
|
||||||
|
'status' => '450',
|
||||||
|
'remote_mta' => 'unknown[1.2.3.4]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$failedDelivery = FailedDelivery::first();
|
||||||
|
$this->assertEquals('s@x.com', $failedDelivery->sender);
|
||||||
|
$this->assertEquals('test@anonaddy.me', $failedDelivery->destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_skips_missing_alias()
|
||||||
|
{
|
||||||
|
$logContent = "Mar 17 10:30:00 server postfix/smtpd[12345]: NOQUEUE: reject: RCPT from unknown[1.2.3.4]: 450 4.7.1 Client host rejected; from=<s@x.com> to=<nobody@anonaddy.me> proto=ESMTP helo=<1.2.3.4>\n";
|
||||||
|
file_put_contents($this->logPath, $logContent);
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('failed_deliveries', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_handles_log_rotation_and_maintains_position()
|
||||||
|
{
|
||||||
|
$alias = Alias::factory()->create(['user_id' => $this->user->id, 'email' => 'test@anonaddy.me']);
|
||||||
|
|
||||||
|
$logContent1 = "Mar 17 10:30:00 server postfix/smtpd[12345]: NOQUEUE: reject: RCPT from unknown[1.2.3.4]: 450 4.7.1 Client host rejected; from=<s@x.com> to=<test@anonaddy.me>\n";
|
||||||
|
file_put_contents($this->logPath, $logContent1);
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
$this->assertDatabaseCount('failed_deliveries', 1);
|
||||||
|
|
||||||
|
// Add a second line to same file
|
||||||
|
$logContent2 = "Mar 17 10:31:00 server postfix/smtpd[12345]: NOQUEUE: reject: RCPT from unknown[1.2.3.4]: 450 4.7.1 Client host rejected; from=<b@x.com> to=<test@anonaddy.me>\n";
|
||||||
|
file_put_contents($this->logPath, $logContent1.$logContent2);
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
$this->assertDatabaseCount('failed_deliveries', 2);
|
||||||
|
|
||||||
|
// Simulate log rotation (file smaller)
|
||||||
|
$logContent3 = "Mar 17 10:32:00 server postfix/smtpd[12345]: NOQUEUE: reject: RCPT from unknown[1.2.3.4]: 450 4.7.1 Client host rejected; from=<c@x.com> to=<test@anonaddy.me>\n";
|
||||||
|
file_put_contents($this->logPath, $logContent3);
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
$this->assertDatabaseCount('failed_deliveries', 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_prevents_duplicate_rejections()
|
||||||
|
{
|
||||||
|
$alias = Alias::factory()->create(['user_id' => $this->user->id, 'email' => 'test@anonaddy.me']);
|
||||||
|
|
||||||
|
$logContent = "Mar 17 10:30:00 server postfix/smtpd[12345]: NOQUEUE: reject: RCPT from unknown[1.2.3.4]: 450 4.7.1 Client host rejected; from=<s@x.com> to=<test@anonaddy.me>\n";
|
||||||
|
file_put_contents($this->logPath, $logContent);
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
$this->assertDatabaseCount('failed_deliveries', 1);
|
||||||
|
|
||||||
|
// Reset position to force re-reading the same line
|
||||||
|
Storage::disk('local')->put('postfix_log_position.txt', '0');
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
$this->assertDatabaseCount('failed_deliveries', 1); // Should not duplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_parses_milter_reject_lines()
|
||||||
|
{
|
||||||
|
$alias = Alias::factory()->create(['user_id' => $this->user->id, 'email' => 'test@anonaddy.com']);
|
||||||
|
|
||||||
|
$logContent = "Mar 18 12:53:05 mail2 postfix/cleanup[1661539]: 7EB9BFF16A: milter-reject: END-OF-MESSAGE from mx.abc.eu[86.106.123.126]: 5.7.1 Spam message rejected; from=<noreply@hi.market> to=<test@anonaddy.com> proto=ESMTP helo=<mx.abc.eu>\n";
|
||||||
|
file_put_contents($this->logPath, $logContent);
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('failed_deliveries', [
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'alias_id' => $alias->id,
|
||||||
|
'email_type' => 'IR',
|
||||||
|
'code' => '5.7.1 Spam message rejected',
|
||||||
|
'status' => '5.7.1',
|
||||||
|
'remote_mta' => 'mx.abc.eu[86.106.123.126]',
|
||||||
|
'bounce_type' => 'spam',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$failedDelivery = FailedDelivery::first();
|
||||||
|
$this->assertEquals('noreply@hi.market', $failedDelivery->sender);
|
||||||
|
$this->assertEquals('test@anonaddy.com', $failedDelivery->destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_parses_discard_lines()
|
||||||
|
{
|
||||||
|
$alias = Alias::factory()->create(['user_id' => $this->user->id, 'email' => 'caloric.test@anonaddy.com']);
|
||||||
|
|
||||||
|
$logContent = "Mar 18 06:55:15 mail2 postfix/smtpd[1491842]: NOQUEUE: discard: RCPT from a.b.com[52.48.1.81]: <caloric.test@anonaddy.com>: Recipient address is inactive alias; from=<takedown@b.com> to=<caloric.test@anonaddy.com> proto=SMTP helo=<a.b.com>\n";
|
||||||
|
file_put_contents($this->logPath, $logContent);
|
||||||
|
|
||||||
|
$this->artisan('anonaddy:parse-postfix-mail-log')->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('failed_deliveries', [
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'alias_id' => $alias->id,
|
||||||
|
'email_type' => 'IR',
|
||||||
|
'code' => 'Recipient address is inactive alias',
|
||||||
|
'status' => '',
|
||||||
|
'remote_mta' => 'a.b.com[52.48.1.81]',
|
||||||
|
'bounce_type' => 'hard',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$failedDelivery = FailedDelivery::first();
|
||||||
|
$this->assertEquals('takedown@b.com', $failedDelivery->sender);
|
||||||
|
$this->assertEquals('caloric.test@anonaddy.com', $failedDelivery->destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ class ShowFailedDeliveriesTest extends TestCase
|
|||||||
|
|
||||||
$response->assertSuccessful();
|
$response->assertSuccessful();
|
||||||
$response->assertInertia(fn (Assert $page) => $page
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
->has('initialRows', 3, fn (Assert $page) => $page
|
->has('initialRows.data', 3, fn (Assert $page) => $page
|
||||||
->where('user_id', $this->user->id)
|
->where('user_id', $this->user->id)
|
||||||
->etc()
|
->etc()
|
||||||
)
|
)
|
||||||
@@ -61,13 +61,71 @@ class ShowFailedDeliveriesTest extends TestCase
|
|||||||
|
|
||||||
$response->assertSuccessful();
|
$response->assertSuccessful();
|
||||||
$response->assertInertia(fn (Assert $page) => $page
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
->has('initialRows', 3, fn (Assert $page) => $page
|
->has('initialRows.data', 3, fn (Assert $page) => $page
|
||||||
->where('user_id', $this->user->id)
|
->where('user_id', $this->user->id)
|
||||||
->etc()
|
->etc()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$this->assertTrue($response->data('page')['props']['initialRows'][0]['id'] === $b->id);
|
$this->assertTrue($response->data('page')['props']['initialRows']['data'][0]['id'] === $b->id);
|
||||||
$this->assertTrue($response->data('page')['props']['initialRows'][1]['id'] === $c->id);
|
$this->assertTrue($response->data('page')['props']['initialRows']['data'][1]['id'] === $c->id);
|
||||||
$this->assertTrue($response->data('page')['props']['initialRows'][2]['id'] === $a->id);
|
$this->assertTrue($response->data('page')['props']['initialRows']['data'][2]['id'] === $a->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function user_can_filter_by_inbound_rejections()
|
||||||
|
{
|
||||||
|
FailedDelivery::factory()->count(2)->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => 'IR',
|
||||||
|
]);
|
||||||
|
FailedDelivery::factory()->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => 'F',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get('/failed-deliveries?filter=inbound');
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
|
->has('initialRows.data', 2)
|
||||||
|
->where('initialFilter', 'inbound')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function user_can_filter_by_outbound_bounces()
|
||||||
|
{
|
||||||
|
FailedDelivery::factory()->count(2)->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => 'IR',
|
||||||
|
]);
|
||||||
|
FailedDelivery::factory()->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => 'F',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get('/failed-deliveries?filter=outbound');
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
|
->has('initialRows.data', 1)
|
||||||
|
->where('initialFilter', 'outbound')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function user_can_paginate_failed_deliveries()
|
||||||
|
{
|
||||||
|
FailedDelivery::factory()->count(30)->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get('/failed-deliveries?page_size=50');
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
|
->has('initialRows.data', 30)
|
||||||
|
->where('initialPageSize', 50)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user