Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fca7d88894 | ||
![]() |
67c824ce1c | ||
![]() |
abab703ecf | ||
![]() |
e7045cc7f7 | ||
![]() |
c832b1b556 | ||
![]() |
aa0addeacd | ||
![]() |
adddee0ba1 | ||
![]() |
5d0270d880 | ||
![]() |
16933763d0 | ||
![]() |
7f2ea49651 | ||
![]() |
3f789f53ef | ||
![]() |
7916d0c004 | ||
![]() |
28d685de61 | ||
![]() |
28cf7b475e | ||
![]() |
18e1223b90 | ||
![]() |
0e75d42e3d |
95 changed files with 3363 additions and 2038 deletions
|
@ -409,7 +409,7 @@ If you get close to your limit (over 80%) you'll be sent an email letting you kn
|
|||
|
||||
## Can I login using an additional username?
|
||||
|
||||
Yes, you can login with any of your usernames. You can add 1 additional username as a Lite user and up to 10 additional usernames as a Pro user for totals of 2 and 11 respectively (including the one you signed up with).
|
||||
Yes, you can login with any of your usernames. You can add 5 additional username as a Lite user and up to 20 additional usernames as a Pro user for totals of 6 and 21 respectively (including the one you signed up with).
|
||||
|
||||
## I'm not receiving any emails, what's wrong?
|
||||
|
||||
|
|
|
@ -260,7 +260,6 @@ smtpd_recipient_restrictions =
|
|||
reject_rhsbl_reverse_client dbl.spamhaus.org,
|
||||
reject_rhsbl_sender dbl.spamhaus.org,
|
||||
reject_rbl_client zen.spamhaus.org
|
||||
reject_rbl_client dul.dnsbl.sorbs.net
|
||||
|
||||
# Block clients that speak too early.
|
||||
smtpd_data_restrictions = reject_unauth_pipelining
|
||||
|
@ -1359,11 +1358,8 @@ npm run production
|
|||
# Run any database migrations
|
||||
php artisan migrate --force
|
||||
|
||||
# Clear cache
|
||||
php artisan config:cache
|
||||
php artisan view:cache
|
||||
php artisan route:cache
|
||||
php artisan event:cache
|
||||
# Cache config, events, routes and views
|
||||
php artisan optimize
|
||||
|
||||
# Restart queue workers to reflect changes
|
||||
php artisan queue:restart
|
||||
|
|
|
@ -55,14 +55,14 @@ class CreateUser extends Command
|
|||
'regex:/^[a-zA-Z0-9]*$/',
|
||||
'max:20',
|
||||
'unique:usernames,username',
|
||||
new NotDeletedUsername(),
|
||||
new NotDeletedUsername,
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'email:rfc,dns',
|
||||
'max:254',
|
||||
new RegisterUniqueRecipient(),
|
||||
new NotLocalRecipient(),
|
||||
new RegisterUniqueRecipient,
|
||||
new NotLocalRecipient,
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
|
@ -454,7 +454,7 @@ class ReceiveEmail extends Command
|
|||
}
|
||||
|
||||
if ($user->nearBandwidthLimit() && ! Cache::has("user:{$user->id}:near-bandwidth")) {
|
||||
$user->notify(new NearBandwidthLimit());
|
||||
$user->notify(new NearBandwidthLimit);
|
||||
|
||||
Cache::put("user:{$user->id}:near-bandwidth", now()->toDateTimeString(), now()->addDay());
|
||||
}
|
||||
|
@ -491,7 +491,7 @@ class ReceiveEmail extends Command
|
|||
|
||||
protected function getParser($file)
|
||||
{
|
||||
$parser = new Parser();
|
||||
$parser = new Parser;
|
||||
|
||||
// Fix some edge cases in from name e.g. "\" John Doe \"" <johndoe@example.com>
|
||||
$parser->addMiddleware(function ($mimePart, $next) {
|
||||
|
|
|
@ -84,7 +84,7 @@ class CustomMailer extends Mailer
|
|||
|
||||
$recipient->update(['should_encrypt' => false]);
|
||||
|
||||
$recipient->notify(new GpgKeyExpired());
|
||||
$recipient->notify(new GpgKeyExpired);
|
||||
}
|
||||
|
||||
if ($encryptedSymfonyMessage) {
|
||||
|
@ -101,8 +101,9 @@ class CustomMailer extends Mailer
|
|||
if (isset($data['needsDkimSignature']) && $data['needsDkimSignature'] && ! is_null(config('anonaddy.dkim_signing_key'))) {
|
||||
$dkimSigner = new DkimSigner(config('anonaddy.dkim_signing_key'), $data['aliasDomain'], config('anonaddy.dkim_selector'));
|
||||
|
||||
$options = (new DkimOptions())->headersToIgnore([
|
||||
$options = (new DkimOptions)->headersToIgnore([
|
||||
'List-Unsubscribe',
|
||||
'List-Unsubscribe-Post',
|
||||
'Return-Path',
|
||||
'Feedback-ID',
|
||||
'Content-Type',
|
||||
|
|
|
@ -220,7 +220,7 @@ class OpenPGPEncrypter
|
|||
}
|
||||
|
||||
if (! $this->gnupg) {
|
||||
$this->gnupg = new \gnupg();
|
||||
$this->gnupg = new \gnupg;
|
||||
}
|
||||
|
||||
$this->gnupg->seterrormode(\gnupg::ERROR_EXCEPTION);
|
||||
|
|
|
@ -86,7 +86,7 @@ class EncryptedPart extends AbstractPart
|
|||
|
||||
public function getPreparedHeaders(): Headers
|
||||
{
|
||||
return clone new Headers();
|
||||
return clone new Headers;
|
||||
}
|
||||
|
||||
public function asDebugString(): string
|
||||
|
@ -104,7 +104,7 @@ class EncryptedPart extends AbstractPart
|
|||
|
||||
private function getEncoder(): ContentEncoderInterface
|
||||
{
|
||||
return new RawContentEncoder();
|
||||
return new RawContentEncoder;
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
|
|
|
@ -13,6 +13,6 @@ class AliasExportController extends Controller
|
|||
return back()->withErrors(['aliases_export' => 'You don\'t have any aliases to export.']);
|
||||
}
|
||||
|
||||
return Excel::download(new AliasesExport(), 'aliases-'.now()->toDateString().'.csv');
|
||||
return Excel::download(new AliasesExport, 'aliases-'.now()->toDateString().'.csv');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\GeneralAliasBulkRequest;
|
||||
use App\Http\Requests\RecipientsAliasBulkRequest;
|
||||
use App\Http\Resources\AliasResource;
|
||||
use App\Rules\VerifiedRecipientId;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
|
@ -16,13 +17,8 @@ class AliasBulkController extends Controller
|
|||
$this->middleware('throttle:12,1');
|
||||
}
|
||||
|
||||
public function get(Request $request)
|
||||
public function get(GeneralAliasBulkRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
||||
$aliases = user()->aliases()->withTrashed()
|
||||
->whereIn('id', $request->ids)
|
||||
->get();
|
||||
|
@ -35,13 +31,8 @@ class AliasBulkController extends Controller
|
|||
return AliasResource::collection($aliases);
|
||||
}
|
||||
|
||||
public function activate(Request $request)
|
||||
public function activate(GeneralAliasBulkRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
||||
$aliasesWithTrashed = user()->aliases()->withTrashed()
|
||||
->select(['id', 'user_id', 'active', 'deleted_at'])
|
||||
->where('active', false)
|
||||
|
@ -75,13 +66,8 @@ class AliasBulkController extends Controller
|
|||
], 200);
|
||||
}
|
||||
|
||||
public function deactivate(Request $request)
|
||||
public function deactivate(GeneralAliasBulkRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
||||
$aliasIds = user()->aliases()
|
||||
->where('active', true)
|
||||
->whereIn('id', $request->ids)
|
||||
|
@ -100,13 +86,8 @@ class AliasBulkController extends Controller
|
|||
], 200);
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
public function delete(GeneralAliasBulkRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
||||
$aliasIds = user()->aliases()
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
@ -129,13 +110,8 @@ class AliasBulkController extends Controller
|
|||
], 200);
|
||||
}
|
||||
|
||||
public function forget(Request $request)
|
||||
public function forget(GeneralAliasBulkRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
||||
$aliasIds = user()->aliases()->withTrashed()
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
@ -183,13 +159,8 @@ class AliasBulkController extends Controller
|
|||
], 200);
|
||||
}
|
||||
|
||||
public function restore(Request $request)
|
||||
public function restore(GeneralAliasBulkRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
||||
$aliasIds = user()->aliases()->onlyTrashed()
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
@ -209,7 +180,7 @@ class AliasBulkController extends Controller
|
|||
], 200);
|
||||
}
|
||||
|
||||
public function recipients(Request $request)
|
||||
public function recipients(RecipientsAliasBulkRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
|
@ -217,7 +188,7 @@ class AliasBulkController extends Controller
|
|||
'recipient_ids' => [
|
||||
'array',
|
||||
'max:10',
|
||||
new VerifiedRecipientId(),
|
||||
new VerifiedRecipientId,
|
||||
],
|
||||
'recipient_ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
|
|
@ -29,7 +29,30 @@ class AliasController extends Controller
|
|||
->when($request->input('sort'), function ($query, $sort) {
|
||||
$direction = strpos($sort, '-') === 0 ? 'desc' : 'asc';
|
||||
$sort = ltrim($sort, '-');
|
||||
$compareOperator = $direction === 'desc' ? '>' : '<';
|
||||
|
||||
// If sort is last_used then order by all and return
|
||||
if ($sort === 'last_used') {
|
||||
return $query
|
||||
->orderByRaw(
|
||||
"CASE
|
||||
WHEN (last_forwarded {$compareOperator} last_replied
|
||||
OR (last_forwarded IS NOT NULL
|
||||
AND last_replied IS NULL))
|
||||
AND (last_forwarded {$compareOperator} last_sent
|
||||
OR (last_forwarded IS NOT NULL
|
||||
AND last_sent IS NULL))
|
||||
THEN last_forwarded
|
||||
WHEN last_replied {$compareOperator} last_sent
|
||||
OR (last_replied IS NOT NULL
|
||||
AND last_sent IS NULL)
|
||||
THEN last_replied
|
||||
ELSE last_sent
|
||||
END {$direction}"
|
||||
)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
// If sort is created at then simply return as no need for secondary sorting below
|
||||
if ($sort === 'created_at') {
|
||||
return $query->orderBy($sort, $direction);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ class DomainController extends Controller
|
|||
|
||||
public function store(StoreDomainRequest $request)
|
||||
{
|
||||
$domain = new Domain();
|
||||
$domain = new Domain;
|
||||
$domain->domain = $request->domain;
|
||||
|
||||
if (! $domain->checkVerification()) {
|
||||
|
@ -55,6 +55,10 @@ class DomainController extends Controller
|
|||
$domain->from_name = $request->from_name;
|
||||
}
|
||||
|
||||
if ($request->has('auto_create_regex')) {
|
||||
$domain->auto_create_regex = $request->auto_create_regex;
|
||||
}
|
||||
|
||||
$domain->save();
|
||||
|
||||
return new DomainResource($domain->refresh()->load('defaultRecipient')->loadCount('aliases'));
|
||||
|
|
|
@ -12,7 +12,7 @@ class RecipientKeyController extends Controller
|
|||
|
||||
public function __construct()
|
||||
{
|
||||
$this->gnupg = new \gnupg();
|
||||
$this->gnupg = new \gnupg;
|
||||
}
|
||||
|
||||
public function update(UpdateRecipientKeyRequest $request, $id)
|
||||
|
|
|
@ -46,6 +46,10 @@ class UsernameController extends Controller
|
|||
$username->from_name = $request->from_name;
|
||||
}
|
||||
|
||||
if ($request->has('auto_create_regex')) {
|
||||
$username->auto_create_regex = $request->auto_create_regex;
|
||||
}
|
||||
|
||||
$username->save();
|
||||
|
||||
return new UsernameResource($username->refresh()->load('defaultRecipient')->loadCount('aliases'));
|
||||
|
|
|
@ -6,9 +6,12 @@ use App\Facades\Webauthn;
|
|||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ApiAuthenticationLoginRequest;
|
||||
use App\Http\Requests\ApiAuthenticationMfaRequest;
|
||||
use App\Http\Requests\DestroyAccountRequest;
|
||||
use App\Jobs\DeleteAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Username;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
@ -20,34 +23,50 @@ class ApiAuthenticationController extends Controller
|
|||
public function __construct()
|
||||
{
|
||||
$this->middleware('throttle:3,1');
|
||||
|
||||
$this->middleware(['auth:sanctum', 'verified'])->only(['logout', 'destroy']);
|
||||
}
|
||||
|
||||
public function login(ApiAuthenticationLoginRequest $request)
|
||||
{
|
||||
$user = Username::firstWhere('username', $request->username)?->user;
|
||||
$user = Username::select(['user_id', 'username'])->firstWhere('username', $request->username)?->user;
|
||||
|
||||
if (! $user || ! Hash::check($request->password, $user->password)) {
|
||||
return response()->json([
|
||||
'error' => 'The provided credentials are incorrect',
|
||||
'message' => 'The provided credentials are incorrect.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
if (! $user->hasVerifiedDefaultRecipient()) {
|
||||
return response()->json([
|
||||
'message' => 'Your email address is not verified.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Check if user has 2FA enabled, if needs OTP then return mfa_key
|
||||
if ($user->two_factor_enabled) {
|
||||
return response()->json([
|
||||
'message' => "OTP required, please make a request to /api/auth/mfa with the 'mfa_key', 'otp' and 'device_name' including a 'X-CSRF-TOKEN' header",
|
||||
'message' => "OTP required, please make a request to /api/auth/mfa with the 'mfa_key', 'otp' and 'device_name' including a 'X-CSRF-TOKEN' header.",
|
||||
'mfa_key' => Crypt::encryptString($user->id.'|'.config('anonaddy.secret').'|'.Carbon::now()->addMinutes(5)->getTimestamp()),
|
||||
'csrf_token' => csrf_token(),
|
||||
], 422);
|
||||
} elseif (Webauthn::enabled($user)) {
|
||||
// If WebAuthn is enabled then return currently unsupported message
|
||||
return response()->json([
|
||||
'error' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead',
|
||||
'message' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
// day, week, month, year or null
|
||||
if ($request->expiration) {
|
||||
$method = 'add'.ucfirst($request->expiration);
|
||||
$expiration = now()->{$method}();
|
||||
} else {
|
||||
$expiration = null;
|
||||
}
|
||||
|
||||
// Token expires after 3 months, user must re-login
|
||||
$newToken = $user->createToken($request->device_name, ['*'], now()->addMonths(3));
|
||||
$newToken = $user->createToken($request->device_name, ['*'], $expiration);
|
||||
$token = $newToken->accessToken;
|
||||
|
||||
// If the user doesn't use 2FA then return the new API key
|
||||
|
@ -65,7 +84,7 @@ class ApiAuthenticationController extends Controller
|
|||
$mfaKey = Crypt::decryptString($request->mfa_key);
|
||||
} catch (DecryptException $e) {
|
||||
return response()->json([
|
||||
'error' => 'Invalid mfa_key',
|
||||
'message' => 'Invalid mfa_key.',
|
||||
], 401);
|
||||
}
|
||||
$parts = explode('|', $mfaKey, 3);
|
||||
|
@ -73,26 +92,29 @@ class ApiAuthenticationController extends Controller
|
|||
$user = User::find($parts[0]);
|
||||
|
||||
if (! $user || $parts[1] !== config('anonaddy.secret')) {
|
||||
|
||||
return response()->json([
|
||||
'error' => 'Invalid mfa_key',
|
||||
'message' => 'Invalid mfa_key.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Check if the mfa_key has expired
|
||||
if (Carbon::now()->getTimestamp() > $parts[2]) {
|
||||
|
||||
return response()->json([
|
||||
'error' => 'mfa_key expired, please request a new one at /api/auth/login',
|
||||
'message' => 'mfa_key expired, please request a new one at /api/auth/login.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
$google2fa = new Google2FA();
|
||||
$google2fa = new Google2FA;
|
||||
$lastTimeStamp = Cache::get('2fa_ts:'.$user->id, 0);
|
||||
|
||||
$timestamp = $google2fa->verifyKeyNewer($user->two_factor_secret, $request->otp, $lastTimeStamp, config('google2fa.window'));
|
||||
|
||||
if (! $timestamp) {
|
||||
|
||||
return response()->json([
|
||||
'error' => 'The \'One Time Password\' typed was wrong',
|
||||
'message' => 'The \'One Time Password\' typed was wrong.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
|
@ -100,7 +122,15 @@ class ApiAuthenticationController extends Controller
|
|||
Cache::put('2fa_ts:'.$user->id, $timestamp, now()->addMinutes(5));
|
||||
}
|
||||
|
||||
$newToken = $user->createToken($request->device_name, ['*'], now()->addMonths(3));
|
||||
// day, week, month, year or null
|
||||
if ($request->expiration) {
|
||||
$method = 'add'.ucfirst($request->expiration);
|
||||
$expiration = now()->{$method}();
|
||||
} else {
|
||||
$expiration = null;
|
||||
}
|
||||
|
||||
$newToken = $user->createToken($request->device_name, ['*'], $expiration);
|
||||
$token = $newToken->accessToken;
|
||||
|
||||
return response()->json([
|
||||
|
@ -110,4 +140,26 @@ class ApiAuthenticationController extends Controller
|
|||
'expires_at' => $token->expires_at?->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$token = $request->user()?->currentAccessToken();
|
||||
|
||||
if (! $token) {
|
||||
return response()->json([
|
||||
'message', 'API key not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$token->delete();
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
public function destroy(DestroyAccountRequest $request)
|
||||
{
|
||||
DeleteAccount::dispatch($request->user());
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ class PersonalAccessTokenController extends Controller
|
|||
return [
|
||||
'token' => new PersonalAccessTokenResource($token->accessToken),
|
||||
'accessToken' => $accessToken,
|
||||
'qrCode' => (new QRCode())->render(config('app.url').'|'.$accessToken),
|
||||
'qrCode' => (new QRCode)->render(config('app.url').'|'.$accessToken),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -79,8 +79,8 @@ class RegisterController extends Controller
|
|||
'regex:/^[a-zA-Z0-9]*$/',
|
||||
'max:20',
|
||||
'unique:usernames,username',
|
||||
new NotBlacklisted(),
|
||||
new NotDeletedUsername(),
|
||||
new NotBlacklisted,
|
||||
new NotDeletedUsername,
|
||||
],
|
||||
'email' => [
|
||||
'bail',
|
||||
|
@ -88,8 +88,8 @@ class RegisterController extends Controller
|
|||
'email:rfc,dns',
|
||||
'max:254',
|
||||
'confirmed',
|
||||
new RegisterUniqueRecipient(),
|
||||
new NotLocalRecipient(),
|
||||
new RegisterUniqueRecipient,
|
||||
new NotLocalRecipient,
|
||||
],
|
||||
'password' => ['required', Password::defaults()],
|
||||
], [
|
||||
|
|
|
@ -58,6 +58,7 @@ class ShowAliasController extends Controller
|
|||
'last_blocked',
|
||||
'last_replied',
|
||||
'last_sent',
|
||||
'last_used',
|
||||
'active',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
|
@ -73,6 +74,7 @@ class ShowAliasController extends Controller
|
|||
'-last_blocked',
|
||||
'-last_replied',
|
||||
'-last_sent',
|
||||
'-last_used',
|
||||
'-active',
|
||||
'-created_at',
|
||||
'-updated_at',
|
||||
|
@ -95,10 +97,12 @@ class ShowAliasController extends Controller
|
|||
|
||||
$sort = $request->session()->get('aliasesSort', 'created_at');
|
||||
$direction = $request->session()->get('aliasesSortDirection', 'desc');
|
||||
$compareOperator = $request->session()->get('aliasesSortCompareOperator', '>');
|
||||
|
||||
if ($request->has('sort')) {
|
||||
$direction = strpos($request->input('sort'), '-') === 0 ? 'desc' : 'asc';
|
||||
$sort = ltrim($request->input('sort'), '-');
|
||||
$compareOperator = $direction === 'desc' ? '>' : '<';
|
||||
|
||||
$request->session()->put('aliasesSort', $sort);
|
||||
$request->session()->put('aliasesSortDirection', $direction);
|
||||
|
@ -115,11 +119,32 @@ class ShowAliasController extends Controller
|
|||
->when($request->input('username'), function ($query, $id) {
|
||||
return $query->belongsToAliasable('App\Models\Username', $id);
|
||||
})
|
||||
->when($sort !== 'created_at' || $direction !== 'desc', function ($query) use ($sort, $direction) {
|
||||
->when($sort !== 'created_at' || $direction !== 'desc', function ($query) use ($sort, $direction, $compareOperator) {
|
||||
if ($sort === 'created_at') {
|
||||
return $query->orderBy($sort, $direction);
|
||||
}
|
||||
|
||||
// If sort is last_used then order by all and return
|
||||
if ($sort === 'last_used') {
|
||||
return $query
|
||||
->orderByRaw(
|
||||
"CASE
|
||||
WHEN (last_forwarded {$compareOperator} last_replied
|
||||
OR (last_forwarded IS NOT NULL
|
||||
AND last_replied IS NULL))
|
||||
AND (last_forwarded {$compareOperator} last_sent
|
||||
OR (last_forwarded IS NOT NULL
|
||||
AND last_sent IS NULL))
|
||||
THEN last_forwarded
|
||||
WHEN last_replied {$compareOperator} last_sent
|
||||
OR (last_replied IS NOT NULL
|
||||
AND last_sent IS NULL)
|
||||
THEN last_replied
|
||||
ELSE last_sent
|
||||
END {$direction}"
|
||||
)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
// Secondary order by latest first
|
||||
return $query
|
||||
->orderBy($sort, $direction)
|
||||
|
|
|
@ -47,7 +47,7 @@ class ShowDomainController extends Controller
|
|||
$domain = user()->domains()->findOrFail($id);
|
||||
|
||||
return Inertia::render('Domains/Edit', [
|
||||
'initialDomain' => $domain->only(['id', 'user_id', 'domain', 'description', 'from_name', 'domain_sending_verified_at', 'domain_mx_validated_at', 'updated_at']),
|
||||
'initialDomain' => $domain->only(['id', 'user_id', 'domain', 'description', 'from_name', 'domain_sending_verified_at', 'domain_mx_validated_at', 'auto_create_regex', 'updated_at']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ class ShowUsernameController extends Controller
|
|||
$username = user()->usernames()->findOrFail($id);
|
||||
|
||||
return Inertia::render('Usernames/Edit', [
|
||||
'initialUsername' => $username->only(['id', 'user_id', 'username', 'description', 'from_name', 'can_login', 'updated_at']),
|
||||
'initialUsername' => $username->only(['id', 'user_id', 'username', 'description', 'from_name', 'can_login', 'auto_create_regex', 'updated_at']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
31
app/Http/Controllers/TestAutoCreateRegexController.php
Normal file
31
app/Http/Controllers/TestAutoCreateRegexController.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\TestAutoCreateRegexRequest;
|
||||
|
||||
class TestAutoCreateRegexController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('throttle:60,1');
|
||||
}
|
||||
|
||||
public function index(TestAutoCreateRegexRequest $request)
|
||||
{
|
||||
$query = $request->resource === 'username' ? user()->usernames() : user()->domains();
|
||||
|
||||
return response()->json([
|
||||
'success' => $query
|
||||
->where('id', $request->id)
|
||||
->whereNotNull('auto_create_regex')
|
||||
->whereRaw('? REGEXP auto_create_regex', [$request->local_part])
|
||||
->exists(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -24,9 +24,27 @@ class ApiAuthenticationLoginRequest extends FormRequest
|
|||
public function rules()
|
||||
{
|
||||
return [
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'device_name' => 'required|string|max:50',
|
||||
'username' => [
|
||||
'required',
|
||||
'regex:/^[a-zA-Z0-9]*$/',
|
||||
'min:1',
|
||||
'max:20',
|
||||
],
|
||||
'password' => [
|
||||
'required',
|
||||
'string',
|
||||
],
|
||||
'device_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:50',
|
||||
],
|
||||
'expiration' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:5',
|
||||
'in:day,week,month,year',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ class EditDefaultRecipientRequest extends FormRequest
|
|||
'required',
|
||||
'email:rfc,dns',
|
||||
'max:254',
|
||||
new RegisterUniqueRecipient(),
|
||||
new RegisterUniqueRecipient,
|
||||
'not_in:'.$this->user()->email,
|
||||
],
|
||||
'current' => 'required|string|current_password',
|
||||
|
|
40
app/Http/Requests/GeneralAliasBulkRequest.php
Normal file
40
app/Http/Requests/GeneralAliasBulkRequest.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class GeneralAliasBulkRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'ids' => Arr::whereNotNull($this->ids ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -75,6 +75,7 @@ class IndexAliasRequest extends FormRequest
|
|||
'last_blocked',
|
||||
'last_replied',
|
||||
'last_sent',
|
||||
'last_used',
|
||||
'active',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
|
@ -90,6 +91,7 @@ class IndexAliasRequest extends FormRequest
|
|||
'-last_blocked',
|
||||
'-last_replied',
|
||||
'-last_sent',
|
||||
'-last_used',
|
||||
'-active',
|
||||
'-created_at',
|
||||
'-updated_at',
|
||||
|
|
47
app/Http/Requests/RecipientsAliasBulkRequest.php
Normal file
47
app/Http/Requests/RecipientsAliasBulkRequest.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\VerifiedRecipientId;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class RecipientsAliasBulkRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'ids' => Arr::whereNotNull($this->ids ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
'recipient_ids' => [
|
||||
'array',
|
||||
'max:10',
|
||||
new VerifiedRecipientId,
|
||||
],
|
||||
'recipient_ids.*' => 'required|uuid|distinct',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ class StoreAliasRecipientRequest extends FormRequest
|
|||
'bail',
|
||||
'array',
|
||||
'max:10',
|
||||
new VerifiedRecipientId(),
|
||||
new VerifiedRecipientId,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ class StoreAliasRequest extends FormRequest
|
|||
'nullable',
|
||||
'array',
|
||||
'max:10',
|
||||
new VerifiedRecipientId(),
|
||||
new VerifiedRecipientId,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class StoreAliasRequest extends FormRequest
|
|||
Rule::unique('aliases', 'local_part')->where(function ($query) {
|
||||
return $query->where('domain', $this->validationData()['domain']);
|
||||
}),
|
||||
new ValidAliasLocalPart(),
|
||||
new ValidAliasLocalPart,
|
||||
], function () {
|
||||
$format = $this->validationData()['format'] ?? 'random_characters';
|
||||
|
||||
|
|
|
@ -31,11 +31,11 @@ class StoreDomainRequest extends FormRequest
|
|||
'bail',
|
||||
'required',
|
||||
'string',
|
||||
'max:50',
|
||||
'max:100',
|
||||
'unique:domains',
|
||||
new ValidDomain(),
|
||||
new NotLocalDomain(),
|
||||
new NotUsedAsRecipientDomain(),
|
||||
new ValidDomain,
|
||||
new NotLocalDomain,
|
||||
new NotUsedAsRecipientDomain,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ class StoreRecipientRequest extends FormRequest
|
|||
'string',
|
||||
'max:254',
|
||||
'email:rfc',
|
||||
new UniqueRecipient(),
|
||||
new NotLocalRecipient(),
|
||||
new UniqueRecipient,
|
||||
new NotLocalRecipient,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class StoreReorderRuleRequest extends FormRequest
|
|||
'ids' => [
|
||||
'required',
|
||||
'array',
|
||||
new ValidRuleId(),
|
||||
new ValidRuleId,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\ValidRegex;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
|
@ -45,7 +46,6 @@ class StoreRuleRequest extends FormRequest
|
|||
]),
|
||||
],
|
||||
'conditions.*.match' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
Rule::in([
|
||||
'is exactly',
|
||||
|
@ -64,9 +64,16 @@ class StoreRuleRequest extends FormRequest
|
|||
'min:1',
|
||||
'max:10',
|
||||
],
|
||||
'conditions.*.values.*' => [
|
||||
'distinct',
|
||||
],
|
||||
'conditions.*.values.*' => Rule::forEach(function ($value, $attribute, $data) {
|
||||
if (in_array(array_values($data)[1], ['matches regex', 'does not match regex'])) {
|
||||
return [
|
||||
new ValidRegex,
|
||||
'distinct',
|
||||
];
|
||||
}
|
||||
|
||||
return ['distinct'];
|
||||
}),
|
||||
'actions' => [
|
||||
'required',
|
||||
'array',
|
||||
|
|
|
@ -32,8 +32,8 @@ class StoreUsernameRequest extends FormRequest
|
|||
'regex:/^[a-zA-Z0-9]*$/',
|
||||
'max:20',
|
||||
'unique:usernames,username',
|
||||
new NotBlacklisted(),
|
||||
new NotDeletedUsername(),
|
||||
new NotBlacklisted,
|
||||
new NotDeletedUsername,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
40
app/Http/Requests/TestAutoCreateRegexRequest.php
Normal file
40
app/Http/Requests/TestAutoCreateRegexRequest.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\ValidAliasLocalPart;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class TestAutoCreateRegexRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'resource' => [
|
||||
'required',
|
||||
'in:username,domain',
|
||||
],
|
||||
'id' => [
|
||||
'required',
|
||||
'uuid',
|
||||
],
|
||||
'local_part' => [
|
||||
'required',
|
||||
new ValidAliasLocalPart,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\ValidRegex;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateDomainRequest extends FormRequest
|
||||
|
@ -26,6 +27,12 @@ class UpdateDomainRequest extends FormRequest
|
|||
return [
|
||||
'description' => 'nullable|max:200',
|
||||
'from_name' => 'nullable|string|max:50',
|
||||
'auto_create_regex' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:100',
|
||||
new ValidRegex,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\ValidRegex;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateUsernameRequest extends FormRequest
|
||||
|
@ -26,6 +27,12 @@ class UpdateUsernameRequest extends FormRequest
|
|||
return [
|
||||
'description' => 'nullable|max:200',
|
||||
'from_name' => 'nullable|string|max:50',
|
||||
'auto_create_regex' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:100',
|
||||
new ValidRegex,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ class DomainResource extends JsonResource
|
|||
'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
|
||||
'active' => $this->active,
|
||||
'catch_all' => $this->catch_all,
|
||||
'auto_create_regex' => $this->auto_create_regex,
|
||||
'domain_verified_at' => $this->domain_verified_at?->toDateTimeString(),
|
||||
'domain_mx_validated_at' => $this->domain_mx_validated_at?->toDateTimeString(),
|
||||
'domain_sending_verified_at' => $this->domain_sending_verified_at?->toDateTimeString(),
|
||||
|
|
|
@ -30,7 +30,7 @@ class UserResource extends JsonResource
|
|||
'banner_location' => $this->banner_location,
|
||||
'bandwidth' => $this->bandwidth,
|
||||
'bandwidth_limit' => $this->getBandwidthLimit(),
|
||||
'username_count' => $this->username_count,
|
||||
'username_count' => $this->usernames()->count(),
|
||||
'username_limit' => config('anonaddy.additional_username_limit'),
|
||||
'default_username_id' => $this->default_username_id,
|
||||
'default_recipient_id' => $this->default_recipient_id,
|
||||
|
|
|
@ -19,6 +19,7 @@ class UsernameResource extends JsonResource
|
|||
'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
|
||||
'active' => $this->active,
|
||||
'catch_all' => $this->catch_all,
|
||||
'auto_create_regex' => $this->auto_create_regex,
|
||||
'can_login' => $this->can_login,
|
||||
'created_at' => $this->created_at->toDateTimeString(),
|
||||
'updated_at' => $this->updated_at->toDateTimeString(),
|
||||
|
|
|
@ -158,7 +158,7 @@ class AliasesImport implements ShouldQueue, SkipsEmptyRows, SkipsOnError, SkipsO
|
|||
'required',
|
||||
'max:64',
|
||||
'string',
|
||||
new ValidAliasLocalPart(),
|
||||
new ValidAliasLocalPart,
|
||||
],
|
||||
'domain' => [
|
||||
'bail',
|
||||
|
|
|
@ -27,7 +27,7 @@ class SendIncorrectOtpNotification
|
|||
// Log in auth.log
|
||||
Log::channel('auth')->info('Failed OTP Notification sent: '.$user->username);
|
||||
|
||||
$user->notify(new IncorrectOtpNotification());
|
||||
$user->notify(new IncorrectOtpNotification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,8 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
protected $listUnsubscribe;
|
||||
|
||||
protected $listUnsubscribePost;
|
||||
|
||||
protected $inReplyTo;
|
||||
|
||||
protected $references;
|
||||
|
@ -144,33 +146,37 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
|
||||
// Create and swap with alias reply-to addresses to allow easy reply-all
|
||||
if (count($this->tos)) {
|
||||
$this->tos = collect($this->tos)
|
||||
->map(function ($to) {
|
||||
// Leave alias email To as it is
|
||||
if (stripEmailExtension($to['address']) === $this->alias->email) {
|
||||
return [
|
||||
'display' => $to['display'] != $to['address'] ? $to['display'] : null,
|
||||
'address' => $this->alias->email,
|
||||
];
|
||||
}
|
||||
|
||||
$this->tos = collect($this->tos)
|
||||
->when(! count($this->tos), function ($tos) {
|
||||
return $tos->push([
|
||||
'display' => null,
|
||||
'address' => $this->alias->email,
|
||||
]);
|
||||
})
|
||||
->map(function ($to) {
|
||||
// Leave alias email To as it is
|
||||
if (stripEmailExtension($to['address']) === $this->alias->email) {
|
||||
return [
|
||||
'display' => $to['display'] != $to['address'] ? $to['display'] : null,
|
||||
'address' => $this->alias->local_part.'+'.Str::replaceLast('@', '=', $to['address']).'@'.$this->alias->domain,
|
||||
'address' => $this->alias->email,
|
||||
];
|
||||
})
|
||||
->filter(fn ($to) => filter_var($to['address'], FILTER_VALIDATE_EMAIL))
|
||||
->map(function ($to) {
|
||||
// Only add in display if it exists
|
||||
if ($to['display']) {
|
||||
return $to['display'].' <'.$to['address'].'>';
|
||||
}
|
||||
}
|
||||
|
||||
return '<'.$to['address'].'>';
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
return [
|
||||
'display' => $to['display'] != $to['address'] ? $to['display'] : null,
|
||||
'address' => $this->alias->local_part.'+'.Str::replaceLast('@', '=', $to['address']).'@'.$this->alias->domain,
|
||||
];
|
||||
})
|
||||
->filter(fn ($to) => filter_var($to['address'], FILTER_VALIDATE_EMAIL))
|
||||
->map(function ($to) {
|
||||
// Only add in display if it exists
|
||||
if ($to['display']) {
|
||||
return $to['display'].' <'.$to['address'].'>';
|
||||
}
|
||||
|
||||
return '<'.$to['address'].'>';
|
||||
})
|
||||
->toArray();
|
||||
|
||||
$this->displayFrom = $emailData->display_from;
|
||||
$this->replyToAddress = $emailData->reply_to_address ?? $this->sender;
|
||||
|
@ -183,6 +189,7 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
|||
$this->size = $emailData->size;
|
||||
$this->messageId = $emailData->messageId;
|
||||
$this->listUnsubscribe = $emailData->listUnsubscribe;
|
||||
$this->listUnsubscribePost = $emailData->listUnsubscribePost;
|
||||
$this->inReplyTo = $emailData->inReplyTo;
|
||||
$this->references = $emailData->references;
|
||||
$this->originalEnvelopeFrom = $emailData->originalEnvelopeFrom;
|
||||
|
@ -251,6 +258,12 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
|||
if ($this->listUnsubscribe) {
|
||||
$message->getHeaders()
|
||||
->addTextHeader('List-Unsubscribe', base64_decode($this->listUnsubscribe));
|
||||
|
||||
// Only check if has original List-Unsubscribe
|
||||
if ($this->listUnsubscribePost) {
|
||||
$message->getHeaders()
|
||||
->addTextHeader('List-Unsubscribe-Post', base64_decode($this->listUnsubscribePost));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->inReplyTo) {
|
||||
|
|
|
@ -284,9 +284,11 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
|||
// Replace <alias+hello=example.com@johndoe.anonaddy.com> with <hello@example.com>
|
||||
$destination = $this->email->to[0]['address'];
|
||||
|
||||
// Reply may be HTML but email client added HTML banner plain text version
|
||||
return Str::of(str_ireplace($this->sender, '', $text))
|
||||
->replace($this->alias->local_part.'+'.Str::replaceLast('@', '=', $destination).'@'.$this->alias->domain, $destination)
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mi', '');
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mi', '')
|
||||
->replaceMatches('/(This email was sent to).*?(to deactivate this alias)/mis', '');
|
||||
}
|
||||
|
||||
private function removeRealEmailAndHtmlBanner($html)
|
||||
|
|
|
@ -266,15 +266,16 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
|||
private function removeRealEmailAndTextBanner($text)
|
||||
{
|
||||
return Str::of(str_ireplace($this->sender, '', $text))
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mi', '');
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mis', '')
|
||||
->replaceMatches('/(This email was sent to).*?(to deactivate this alias)/mis', '');
|
||||
}
|
||||
|
||||
private function removeRealEmailAndHtmlBanner($html)
|
||||
{
|
||||
// Reply may be HTML but have a plain text banner
|
||||
return Str::of(str_ireplace($this->sender, '', $html))
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mi', '')
|
||||
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/')."(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mi", '');
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mis', '')
|
||||
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/').'(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mis', '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -41,7 +41,7 @@ class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQue
|
|||
public function build()
|
||||
{
|
||||
return $this
|
||||
->subject('Your addy.io API key expires soon')
|
||||
->subject('Your '.config('app.name').' API key expires soon')
|
||||
->markdown('mail.token_expiring_soon', [
|
||||
'user' => $this->user,
|
||||
'userId' => $this->user->id,
|
||||
|
|
|
@ -40,6 +40,8 @@ class EmailData
|
|||
|
||||
public $listUnsubscribe;
|
||||
|
||||
public $listUnsubscribePost;
|
||||
|
||||
public $inReplyTo;
|
||||
|
||||
public $references;
|
||||
|
@ -85,8 +87,17 @@ class EmailData
|
|||
}
|
||||
}
|
||||
|
||||
$this->ccs = collect($parser->getAddresses('cc'))->all();
|
||||
$this->tos = collect($parser->getAddresses('to'))->all();
|
||||
try {
|
||||
$this->ccs = collect($parser->getAddresses('cc'))->all();
|
||||
} catch (\Throwable $e) {
|
||||
$this->ccs = [];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->tos = collect($parser->getAddresses('to'))->all();
|
||||
} catch (\Throwable $e) {
|
||||
$this->tos = [];
|
||||
}
|
||||
|
||||
if ($originalCc = $parser->getHeader('cc')) {
|
||||
$this->originalCc = $originalCc;
|
||||
|
@ -104,6 +115,7 @@ class EmailData
|
|||
$this->size = $size;
|
||||
$this->messageId = base64_encode(Str::remove(['<', '>'], $parser->getHeader('Message-ID')));
|
||||
$this->listUnsubscribe = base64_encode($parser->getHeader('List-Unsubscribe'));
|
||||
$this->listUnsubscribePost = base64_encode($parser->getHeader('List-Unsubscribe-Post'));
|
||||
$this->inReplyTo = base64_encode($parser->getHeader('In-Reply-To'));
|
||||
$this->references = base64_encode($parser->getHeader('References'));
|
||||
$this->originalEnvelopeFrom = $sender;
|
||||
|
@ -164,7 +176,7 @@ class EmailData
|
|||
} else {
|
||||
if (! str_contains($contentType, '/')) {
|
||||
if (self::$mimeTypes === null) {
|
||||
self::$mimeTypes = new MimeTypes();
|
||||
self::$mimeTypes = new MimeTypes;
|
||||
}
|
||||
$contentType = self::$mimeTypes->getMimeTypes($contentType)[0] ?? 'application/octet-stream';
|
||||
}
|
||||
|
@ -191,7 +203,7 @@ class EmailData
|
|||
private function attemptToDecrypt($part)
|
||||
{
|
||||
try {
|
||||
$gnupg = new \gnupg();
|
||||
$gnupg = new \gnupg;
|
||||
|
||||
$gnupg->cleardecryptkeys();
|
||||
$gnupg->adddecryptkey(config('anonaddy.signing_key_fingerprint'), null);
|
||||
|
@ -202,7 +214,7 @@ class EmailData
|
|||
|
||||
if ($decrypted) {
|
||||
|
||||
$decryptedParser = new Parser();
|
||||
$decryptedParser = new Parser;
|
||||
$decryptedParser->setText($decrypted);
|
||||
|
||||
// Set decrypted data as subject (as may have encrypted subject too), html and text
|
||||
|
@ -223,7 +235,7 @@ class EmailData
|
|||
private function attemptToDecryptInline($text)
|
||||
{
|
||||
try {
|
||||
$gnupg = new \gnupg();
|
||||
$gnupg = new \gnupg;
|
||||
|
||||
$gnupg->cleardecryptkeys();
|
||||
$gnupg->adddecryptkey(config('anonaddy.signing_key_fingerprint'), null);
|
||||
|
|
|
@ -205,7 +205,7 @@ class Recipient extends Model
|
|||
*/
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
$this->notify(new CustomVerifyEmail());
|
||||
$this->notify(new CustomVerifyEmail);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -215,7 +215,7 @@ class Recipient extends Model
|
|||
*/
|
||||
public function sendUsernameReminderNotification()
|
||||
{
|
||||
$this->notify(new UsernameReminder());
|
||||
$this->notify(new UsernameReminder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -414,7 +414,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
*/
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
$this->notify(new CustomVerifyEmail());
|
||||
$this->notify(new CustomVerifyEmail);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -430,6 +430,10 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
|
||||
public function hasVerifiedDefaultRecipient()
|
||||
{
|
||||
if (! isset($this->defaultRecipient->email_verified_at)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! is_null($this->defaultRecipient->email_verified_at);
|
||||
}
|
||||
|
||||
|
@ -499,7 +503,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
|
||||
public function hasReachedUsernameLimit()
|
||||
{
|
||||
return $this->username_count >= config('anonaddy.additional_username_limit');
|
||||
return $this->usernames()->count() >= config('anonaddy.additional_username_limit');
|
||||
}
|
||||
|
||||
public function isVerifiedRecipient($email)
|
||||
|
@ -546,7 +550,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
|
||||
public function deleteKeyFromKeyring($fingerprint): void
|
||||
{
|
||||
$gnupg = new \gnupg();
|
||||
$gnupg = new \gnupg;
|
||||
|
||||
$recipientsUsingFingerprint = $this
|
||||
->recipients()
|
||||
|
|
|
@ -59,7 +59,7 @@ class AliasesImportedNotification extends Notification implements ShouldBeEncryp
|
|||
$recipient = $notifiable->defaultRecipient;
|
||||
$fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
|
||||
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject('Your aliases import has finished')
|
||||
->markdown('mail.aliases_import_finished', [
|
||||
'totalRows' => $this->totalRows,
|
||||
|
|
|
@ -37,7 +37,7 @@ class CustomVerifyEmail extends VerifyEmail implements ShouldBeEncrypted, Should
|
|||
$recipientId = $notifiable instanceof User ? $notifiable->default_recipient_id : $notifiable->id;
|
||||
$userId = $notifiable instanceof User ? $notifiable->id : $notifiable->user_id;
|
||||
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject(Lang::get('Verify Email Address'))
|
||||
->markdown('mail.verify_email', [
|
||||
'verificationUrl' => $verificationUrl,
|
||||
|
|
|
@ -44,7 +44,7 @@ class DefaultRecipientUpdated extends Notification implements ShouldBeEncrypted,
|
|||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject('Your default recipient has just been updated')
|
||||
->markdown('mail.default_recipient_updated', [
|
||||
'defaultRecipient' => $notifiable->email,
|
||||
|
|
|
@ -56,7 +56,7 @@ class DisallowedReplySendAttempt extends Notification implements ShouldBeEncrypt
|
|||
{
|
||||
$fingerprint = $notifiable->should_encrypt ? $notifiable->fingerprint : null;
|
||||
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject('Disallowed reply/send from alias')
|
||||
->markdown('mail.disallowed_reply_send_attempt', [
|
||||
'aliasEmail' => $this->aliasEmail,
|
||||
|
|
|
@ -47,7 +47,7 @@ class DomainMxRecordsInvalid extends Notification implements ShouldBeEncrypted,
|
|||
$recipient = $notifiable->defaultRecipient;
|
||||
$fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
|
||||
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject("Your domain's MX records no longer point to addy.io")
|
||||
->markdown('mail.domain_mx_records_invalid', [
|
||||
'domain' => $this->domain,
|
||||
|
|
|
@ -50,7 +50,7 @@ class DomainUnverifiedForSending extends Notification implements ShouldBeEncrypt
|
|||
$recipient = $notifiable->defaultRecipient;
|
||||
$fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
|
||||
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject('Your domain has been unverified for sending on addy.io')
|
||||
->markdown('mail.domain_unverified_for_sending', [
|
||||
'domain' => $this->domain,
|
||||
|
|
|
@ -58,7 +58,7 @@ class FailedDeliveryNotification extends Notification implements ShouldBeEncrypt
|
|||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject('New failed delivery on addy.io')
|
||||
->markdown('mail.failed_delivery_notification', [
|
||||
'aliasEmail' => $this->aliasEmail,
|
||||
|
|
|
@ -32,7 +32,7 @@ class GpgKeyExpired extends Notification implements ShouldBeEncrypted, ShouldQue
|
|||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject('Your GPG key has expired on addy.io')
|
||||
->markdown('mail.gpg_key_expired', [
|
||||
'recipient' => $notifiable,
|
||||
|
|
|
@ -35,7 +35,7 @@ class IncorrectOtpNotification extends Notification implements ShouldBeEncrypted
|
|||
$recipient = $notifiable->defaultRecipient;
|
||||
$fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
|
||||
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject('Failed Two Factor Authentication Login Attempt')
|
||||
->markdown('mail.failed_login_attempt', [
|
||||
'userId' => $notifiable->id,
|
||||
|
|
|
@ -50,7 +50,7 @@ class NearBandwidthLimit extends Notification implements ShouldBeEncrypted, Shou
|
|||
$recipient = $notifiable->defaultRecipient;
|
||||
$fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
|
||||
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject("You're close to your bandwidth limit for ".$this->month)
|
||||
->markdown('mail.near_bandwidth_limit', [
|
||||
'bandwidthUsage' => $notifiable->bandwidth_mb,
|
||||
|
|
|
@ -54,7 +54,7 @@ class SpamReplySendAttempt extends Notification implements ShouldBeEncrypted, Sh
|
|||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage())
|
||||
return (new MailMessage)
|
||||
->subject('Attempted reply/send from alias has failed')
|
||||
->markdown('mail.spam_reply_send_attempt', [
|
||||
'aliasEmail' => $this->aliasEmail,
|
||||
|
|
|
@ -32,8 +32,8 @@ class UsernameReminder extends Notification implements ShouldBeEncrypted, Should
|
|||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage())
|
||||
->subject('addy.io Username Reminder')
|
||||
return (new MailMessage)
|
||||
->subject(config('app.name').' Username Reminder')
|
||||
->markdown('mail.username_reminder', [
|
||||
'username' => $notifiable->user->username,
|
||||
'userId' => $notifiable->user_id,
|
||||
|
|
|
@ -19,7 +19,7 @@ class ValidDomain implements ValidationRule
|
|||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (! preg_match('/(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/', $value)) {
|
||||
if (! preg_match('/(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z0-9-]{2,63}$)/', $value)) {
|
||||
$fail('Invalid domain name.');
|
||||
}
|
||||
}
|
||||
|
|
21
app/Rules/ValidRegex.php
Normal file
21
app/Rules/ValidRegex.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class ValidRegex implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (@preg_match("/{$value}/", '') === false) {
|
||||
$fail("{$attribute} is an invalid regular expression.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -74,8 +74,8 @@ trait CheckUserRules
|
|||
});
|
||||
break;
|
||||
case 'is not':
|
||||
return $values->contains(function ($value) use ($variable) {
|
||||
return $variable !== $value;
|
||||
return ! $values->contains(function ($value) use ($variable) {
|
||||
return $variable === $value;
|
||||
});
|
||||
break;
|
||||
case 'contains':
|
||||
|
@ -84,8 +84,8 @@ trait CheckUserRules
|
|||
});
|
||||
break;
|
||||
case 'does not contain':
|
||||
return $values->contains(function ($value) use ($variable) {
|
||||
return ! Str::contains($variable, $value);
|
||||
return ! $values->contains(function ($value) use ($variable) {
|
||||
return Str::contains($variable, $value);
|
||||
});
|
||||
break;
|
||||
case 'starts with':
|
||||
|
@ -94,8 +94,8 @@ trait CheckUserRules
|
|||
});
|
||||
break;
|
||||
case 'does not start with':
|
||||
return $values->contains(function ($value) use ($variable) {
|
||||
return ! Str::startsWith($variable, $value);
|
||||
return ! $values->contains(function ($value) use ($variable) {
|
||||
return Str::startsWith($variable, $value);
|
||||
});
|
||||
break;
|
||||
case 'ends with':
|
||||
|
@ -104,11 +104,20 @@ trait CheckUserRules
|
|||
});
|
||||
break;
|
||||
case 'does not end with':
|
||||
return $values->contains(function ($value) use ($variable) {
|
||||
return ! Str::endsWith($variable, $value);
|
||||
return ! $values->contains(function ($value) use ($variable) {
|
||||
return Str::endsWith($variable, $value);
|
||||
});
|
||||
break;
|
||||
case 'matches regex':
|
||||
return $values->contains(function ($value) use ($variable) {
|
||||
return Str::isMatch("/{$value}/", $variable);
|
||||
});
|
||||
break;
|
||||
case 'does not match regex':
|
||||
return ! $values->contains(function ($value) use ($variable) {
|
||||
return Str::isMatch("/{$value}/", $variable);
|
||||
});
|
||||
break;
|
||||
// regex preg_match?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
"laravel/tinker": "^2.7",
|
||||
"laravel/ui": "^4.0",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"mews/captcha": "^3.0.0",
|
||||
"php-mime-mail-parser/php-mime-mail-parser": "^8.0",
|
||||
"mews/captcha": "3.3.3",
|
||||
"php-mime-mail-parser/php-mime-mail-parser": "^9.0",
|
||||
"pragmarx/google2fa-laravel": "^2.0.0",
|
||||
"ramsey/uuid": "^4.0",
|
||||
"tightenco/ziggy": "^2.0.0"
|
||||
|
|
2283
composer.lock
generated
2283
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -13,7 +13,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
'name' => env('APP_NAME', 'addy.io'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
|
@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
|
@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
|
@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
|
@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
|
@ -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('usernames', function (Blueprint $table) {
|
||||
$table->string('auto_create_regex')->after('catch_all')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('domains', function (Blueprint $table) {
|
||||
$table->string('auto_create_regex')->after('catch_all')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('usernames', function (Blueprint $table) {
|
||||
$table->dropColumn('auto_create_regex');
|
||||
});
|
||||
|
||||
Schema::table('domains', function (Blueprint $table) {
|
||||
$table->dropColumn('auto_create_regex');
|
||||
});
|
||||
}
|
||||
};
|
1086
package-lock.json
generated
1086
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -32,7 +32,7 @@ try {
|
|||
$dotenv = Dotenv\Dotenv::create($repository, dirname(__DIR__));
|
||||
$dotenv->load();
|
||||
|
||||
$database = new Database();
|
||||
$database = new Database;
|
||||
|
||||
$database->addConnection([
|
||||
'driver' => 'mysql',
|
||||
|
@ -171,7 +171,7 @@ try {
|
|||
// If the alias is inactive or deleted then increment the blocked count
|
||||
Database::table('aliases')
|
||||
->where('email', $aliasEmail)
|
||||
->increment('emails_blocked', 1, ['last_blocked' => new DateTime()]);
|
||||
->increment('emails_blocked', 1, ['last_blocked' => new DateTime]);
|
||||
|
||||
sendAction($aliasAction);
|
||||
} elseif ($aliasHasSharedDomain || in_array($aliasAction, [ACTION_REJECT, ACTION_DEFER])) {
|
||||
|
@ -191,7 +191,7 @@ try {
|
|||
->leftJoin('users', 'usernames.user_id', '=', 'users.id')
|
||||
->whereRaw('? IN ('.$concatDomainsStatement.')', [$aliasDomain, ...$dotDomains])
|
||||
->selectRaw('CASE
|
||||
WHEN ? AND usernames.catch_all = 0 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 users.reject_until > NOW() THEN ?
|
||||
WHEN users.defer_until > NOW() THEN ?
|
||||
|
@ -199,6 +199,7 @@ try {
|
|||
ELSE "DUNNO"
|
||||
END', [
|
||||
$noAliasExists,
|
||||
$aliasLocalPart,
|
||||
ACTION_DOES_NOT_EXIST,
|
||||
ACTION_USERNAME_DISCARD,
|
||||
ACTION_REJECT,
|
||||
|
@ -214,7 +215,7 @@ try {
|
|||
->leftJoin('users', 'domains.user_id', '=', 'users.id')
|
||||
->where('domains.domain', $aliasDomain)
|
||||
->selectRaw('CASE
|
||||
WHEN ? AND domains.catch_all = 0 THEN ?
|
||||
WHEN ? AND domains.catch_all = 0 AND (domains.auto_create_regex IS NULL OR ? NOT REGEXP domains.auto_create_regex) THEN ?
|
||||
WHEN domains.active = 0 THEN ?
|
||||
WHEN users.reject_until > NOW() THEN ?
|
||||
WHEN users.defer_until > NOW() THEN ?
|
||||
|
@ -222,6 +223,7 @@ try {
|
|||
ELSE "DUNNO"
|
||||
END', [
|
||||
$noAliasExists,
|
||||
$aliasLocalPart,
|
||||
ACTION_DOES_NOT_EXIST,
|
||||
ACTION_DOMAIN_DISCARD,
|
||||
ACTION_REJECT,
|
||||
|
@ -342,5 +344,5 @@ function endsWith($haystack, $needles)
|
|||
|
||||
function logData($data)
|
||||
{
|
||||
file_put_contents(__DIR__.'/../storage/logs/postfix-access-policy.log', '['.(new DateTime())->format('Y-m-d H:i:s').'] '.$data.PHP_EOL, FILE_APPEND);
|
||||
file_put_contents(__DIR__.'/../storage/logs/postfix-access-policy.log', '['.(new DateTime)->format('Y-m-d H:i:s').'] '.$data.PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
|
333
postfix/composer.lock
generated
333
postfix/composer.lock
generated
|
@ -228,24 +228,24 @@
|
|||
},
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"version": "v1.1.2",
|
||||
"version": "v1.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/GrahamCampbell/Result-Type.git",
|
||||
"reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862"
|
||||
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862",
|
||||
"reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862",
|
||||
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
|
||||
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpoption/phpoption": "^1.9.2"
|
||||
"phpoption/phpoption": "^1.9.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
|
||||
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
@ -274,7 +274,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
|
||||
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2"
|
||||
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -286,20 +286,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-11-12T22:16:48+00:00"
|
||||
"time": "2024-07-20T21:45:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/collections",
|
||||
"version": "v11.12.0",
|
||||
"version": "v11.34.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/collections.git",
|
||||
"reference": "6923dc8c83b8c065c3b89cb4c2a4b10a4288ce79"
|
||||
"reference": "fd2103ddc121449a7926fc34a9d220e5b88183c1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/collections/zipball/6923dc8c83b8c065c3b89cb4c2a4b10a4288ce79",
|
||||
"reference": "6923dc8c83b8c065c3b89cb4c2a4b10a4288ce79",
|
||||
"url": "https://api.github.com/repos/illuminate/collections/zipball/fd2103ddc121449a7926fc34a9d220e5b88183c1",
|
||||
"reference": "fd2103ddc121449a7926fc34a9d220e5b88183c1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -341,20 +341,20 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2024-06-21T15:52:51+00:00"
|
||||
"time": "2024-11-27T14:51:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/conditionable",
|
||||
"version": "v11.12.0",
|
||||
"version": "v11.34.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/conditionable.git",
|
||||
"reference": "8a558fec063b6a63da1c3af1d219c0f998edffeb"
|
||||
"reference": "911df1bda950a3b799cf80671764e34eede131c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/conditionable/zipball/8a558fec063b6a63da1c3af1d219c0f998edffeb",
|
||||
"reference": "8a558fec063b6a63da1c3af1d219c0f998edffeb",
|
||||
"url": "https://api.github.com/repos/illuminate/conditionable/zipball/911df1bda950a3b799cf80671764e34eede131c6",
|
||||
"reference": "911df1bda950a3b799cf80671764e34eede131c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -387,20 +387,20 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2024-04-04T17:36:49+00:00"
|
||||
"time": "2024-11-21T16:28:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/container",
|
||||
"version": "v11.12.0",
|
||||
"version": "v11.34.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/container.git",
|
||||
"reference": "af979ecfd6dfa6583eae5dfe2e9a8840358f4ca7"
|
||||
"reference": "b057b0bbb38d7c7524df1ca5c38e7318f4c64d26"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/container/zipball/af979ecfd6dfa6583eae5dfe2e9a8840358f4ca7",
|
||||
"reference": "af979ecfd6dfa6583eae5dfe2e9a8840358f4ca7",
|
||||
"url": "https://api.github.com/repos/illuminate/container/zipball/b057b0bbb38d7c7524df1ca5c38e7318f4c64d26",
|
||||
"reference": "b057b0bbb38d7c7524df1ca5c38e7318f4c64d26",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -438,20 +438,20 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2024-04-04T17:36:49+00:00"
|
||||
"time": "2024-11-21T20:07:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/contracts",
|
||||
"version": "v11.12.0",
|
||||
"version": "v11.34.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/contracts.git",
|
||||
"reference": "86c1331d0b06c59ca21723d8bfc9faaa19430b46"
|
||||
"reference": "184317f701ba20ca265e36808ed54b75b115972d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/contracts/zipball/86c1331d0b06c59ca21723d8bfc9faaa19430b46",
|
||||
"reference": "86c1331d0b06c59ca21723d8bfc9faaa19430b46",
|
||||
"url": "https://api.github.com/repos/illuminate/contracts/zipball/184317f701ba20ca265e36808ed54b75b115972d",
|
||||
"reference": "184317f701ba20ca265e36808ed54b75b115972d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -486,20 +486,20 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2024-05-21T17:42:34+00:00"
|
||||
"time": "2024-11-25T15:33:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/database",
|
||||
"version": "v11.12.0",
|
||||
"version": "v11.34.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/database.git",
|
||||
"reference": "d2bd095eab3d7a1e869b5824bcada7492e447ec7"
|
||||
"reference": "f08bb9ffa1d829cb6f8bb713e5c9cd3a47636ff2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/database/zipball/d2bd095eab3d7a1e869b5824bcada7492e447ec7",
|
||||
"reference": "d2bd095eab3d7a1e869b5824bcada7492e447ec7",
|
||||
"url": "https://api.github.com/repos/illuminate/database/zipball/f08bb9ffa1d829cb6f8bb713e5c9cd3a47636ff2",
|
||||
"reference": "f08bb9ffa1d829cb6f8bb713e5c9cd3a47636ff2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -514,11 +514,12 @@
|
|||
},
|
||||
"suggest": {
|
||||
"ext-filter": "Required to use the Postgres database driver.",
|
||||
"fakerphp/faker": "Required to use the eloquent factory builder (^1.21).",
|
||||
"fakerphp/faker": "Required to use the eloquent factory builder (^1.24).",
|
||||
"illuminate/console": "Required to use the database commands (^11.0).",
|
||||
"illuminate/events": "Required to use the observers with Eloquent (^11.0).",
|
||||
"illuminate/filesystem": "Required to use the migrations (^11.0).",
|
||||
"illuminate/pagination": "Required to paginate the result set (^11.0).",
|
||||
"laravel/serializable-closure": "Required to handle circular references in model serialization (^1.3).",
|
||||
"symfony/finder": "Required to use Eloquent model factories (^7.0)."
|
||||
},
|
||||
"type": "library",
|
||||
|
@ -554,20 +555,20 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2024-06-24T20:24:42+00:00"
|
||||
"time": "2024-11-25T22:44:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/macroable",
|
||||
"version": "v11.12.0",
|
||||
"version": "v11.34.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/macroable.git",
|
||||
"reference": "5b6c7c7c5951e6e8fc22dd7e4363602df8294dfa"
|
||||
"reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/macroable/zipball/5b6c7c7c5951e6e8fc22dd7e4363602df8294dfa",
|
||||
"reference": "5b6c7c7c5951e6e8fc22dd7e4363602df8294dfa",
|
||||
"url": "https://api.github.com/repos/illuminate/macroable/zipball/e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed",
|
||||
"reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -600,20 +601,20 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2024-05-16T21:43:47+00:00"
|
||||
"time": "2024-06-28T20:10:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/support",
|
||||
"version": "v11.12.0",
|
||||
"version": "v11.34.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/support.git",
|
||||
"reference": "1ea237b71cd2e181af36074e2a9dd47f268e5cda"
|
||||
"reference": "2b718a86571baed50fdc5d5748a846c2e58e07eb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/support/zipball/1ea237b71cd2e181af36074e2a9dd47f268e5cda",
|
||||
"reference": "1ea237b71cd2e181af36074e2a9dd47f268e5cda",
|
||||
"url": "https://api.github.com/repos/illuminate/support/zipball/2b718a86571baed50fdc5d5748a846c2e58e07eb",
|
||||
"reference": "2b718a86571baed50fdc5d5748a846c2e58e07eb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -625,9 +626,9 @@
|
|||
"illuminate/conditionable": "^11.0",
|
||||
"illuminate/contracts": "^11.0",
|
||||
"illuminate/macroable": "^11.0",
|
||||
"nesbot/carbon": "^2.72.2|^3.0",
|
||||
"nesbot/carbon": "^2.72.2|^3.4",
|
||||
"php": "^8.2",
|
||||
"voku/portable-ascii": "^2.0"
|
||||
"voku/portable-ascii": "^2.0.2"
|
||||
},
|
||||
"conflict": {
|
||||
"tightenco/collect": "<5.5.33"
|
||||
|
@ -637,12 +638,13 @@
|
|||
},
|
||||
"suggest": {
|
||||
"illuminate/filesystem": "Required to use the composer class (^11.0).",
|
||||
"laravel/serializable-closure": "Required to use the once function (^1.3).",
|
||||
"league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.0.2).",
|
||||
"ramsey/uuid": "Required to use Str::uuid() (^4.7).",
|
||||
"symfony/process": "Required to use the composer class (^7.0).",
|
||||
"symfony/uid": "Required to use Str::ulid() (^7.0).",
|
||||
"symfony/var-dumper": "Required to use the dd function (^7.0).",
|
||||
"vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)."
|
||||
"vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
|
@ -652,6 +654,7 @@
|
|||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"functions.php",
|
||||
"helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
|
@ -674,24 +677,24 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2024-06-24T20:24:42+00:00"
|
||||
"time": "2024-11-27T14:58:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "3.6.0",
|
||||
"version": "3.8.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/briannesbitt/Carbon.git",
|
||||
"reference": "39c8ef752db6865717cc3fba63970c16f057982c"
|
||||
"reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/39c8ef752db6865717cc3fba63970c16f057982c",
|
||||
"reference": "39c8ef752db6865717cc3fba63970c16f057982c",
|
||||
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947",
|
||||
"reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"carbonphp/carbon-doctrine-types": "*",
|
||||
"carbonphp/carbon-doctrine-types": "<100.0",
|
||||
"ext-json": "*",
|
||||
"php": "^8.1",
|
||||
"psr/clock": "^1.0",
|
||||
|
@ -780,7 +783,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-20T15:52:59+00:00"
|
||||
"time": "2024-11-07T17:46:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/constant_time_encoding",
|
||||
|
@ -851,16 +854,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.2",
|
||||
"version": "1.9.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/schmittjoh/php-option.git",
|
||||
"reference": "80735db690fe4fc5c76dfa7f9b770634285fa820"
|
||||
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820",
|
||||
"reference": "80735db690fe4fc5c76dfa7f9b770634285fa820",
|
||||
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
|
||||
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -868,13 +871,13 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
|
||||
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"bamarni-bin": {
|
||||
"bin-links": true,
|
||||
"forward-command": true
|
||||
"forward-command": false
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "1.9-dev"
|
||||
|
@ -910,7 +913,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/schmittjoh/php-option/issues",
|
||||
"source": "https://github.com/schmittjoh/php-option/tree/1.9.2"
|
||||
"source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -922,7 +925,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-11-12T21:59:55+00:00"
|
||||
"time": "2024-07-20T21:41:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/clock",
|
||||
|
@ -1078,16 +1081,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/clock.git",
|
||||
"reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7"
|
||||
"reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7",
|
||||
"reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7",
|
||||
"url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24",
|
||||
"reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1132,7 +1135,7 @@
|
|||
"time"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/clock/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/clock/tree/v7.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1148,24 +1151,91 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.30.0",
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
|
||||
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
|
||||
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.5-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
|
@ -1176,8 +1246,8 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -1211,7 +1281,7 @@
|
|||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1227,24 +1297,24 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
|
||||
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
|
@ -1291,7 +1361,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1307,30 +1377,30 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-19T12:30:46+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
|
||||
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
|
||||
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
|
||||
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -1371,7 +1441,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1387,30 +1457,30 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9"
|
||||
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
|
||||
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
|
||||
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -1447,7 +1517,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1463,24 +1533,25 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-19T12:35:24+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/translation.git",
|
||||
"reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3"
|
||||
"reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3",
|
||||
"reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/dc89e16b44048ceecc879054e5b7f38326ab6cc5",
|
||||
"reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/translation-contracts": "^2.5|^3.0"
|
||||
},
|
||||
|
@ -1541,7 +1612,7 @@
|
|||
"description": "Provides tools to internationalize your application",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/translation/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/translation/tree/v7.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1557,20 +1628,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-11-12T20:47:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation-contracts",
|
||||
"version": "v3.5.0",
|
||||
"version": "v3.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/translation-contracts.git",
|
||||
"reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a"
|
||||
"reference": "4667ff3bd513750603a09c8dedbea942487fb07c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
|
||||
"reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
|
||||
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c",
|
||||
"reference": "4667ff3bd513750603a09c8dedbea942487fb07c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1619,7 +1690,7 @@
|
|||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/translation-contracts/tree/v3.5.0"
|
||||
"source": "https://github.com/symfony/translation-contracts/tree/v3.5.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1635,27 +1706,27 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-04-18T09:32:20+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "vlucas/phpdotenv",
|
||||
"version": "v5.6.0",
|
||||
"version": "v5.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vlucas/phpdotenv.git",
|
||||
"reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4"
|
||||
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
|
||||
"reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
|
||||
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
|
||||
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-pcre": "*",
|
||||
"graham-campbell/result-type": "^1.1.2",
|
||||
"graham-campbell/result-type": "^1.1.3",
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpoption/phpoption": "^1.9.2",
|
||||
"phpoption/phpoption": "^1.9.3",
|
||||
"symfony/polyfill-ctype": "^1.24",
|
||||
"symfony/polyfill-mbstring": "^1.24",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
|
@ -1672,7 +1743,7 @@
|
|||
"extra": {
|
||||
"bamarni-bin": {
|
||||
"bin-links": true,
|
||||
"forward-command": true
|
||||
"forward-command": false
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "5.6-dev"
|
||||
|
@ -1707,7 +1778,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/vlucas/phpdotenv/issues",
|
||||
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0"
|
||||
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1719,20 +1790,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-11-12T22:43:29+00:00"
|
||||
"time": "2024-07-20T21:52:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "voku/portable-ascii",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/voku/portable-ascii.git",
|
||||
"reference": "b56450eed252f6801410d810c8e1727224ae0743"
|
||||
"reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/voku/portable-ascii/zipball/b56450eed252f6801410d810c8e1727224ae0743",
|
||||
"reference": "b56450eed252f6801410d810c8e1727224ae0743",
|
||||
"url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d",
|
||||
"reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1757,7 +1828,7 @@
|
|||
"authors": [
|
||||
{
|
||||
"name": "Lars Moelleken",
|
||||
"homepage": "http://www.moelleken.org/"
|
||||
"homepage": "https://www.moelleken.org/"
|
||||
}
|
||||
],
|
||||
"description": "Portable ASCII library - performance optimized (ascii) string functions for php.",
|
||||
|
@ -1769,7 +1840,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/voku/portable-ascii/issues",
|
||||
"source": "https://github.com/voku/portable-ascii/tree/2.0.1"
|
||||
"source": "https://github.com/voku/portable-ascii/tree/2.0.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1793,7 +1864,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-03-08T17:03:00+00:00"
|
||||
"time": "2024-11-21T01:49:47+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
|
|
@ -284,6 +284,15 @@
|
|||
data-tippy-content="'Select All' is only available when the page size is 25"
|
||||
></div>
|
||||
</span>
|
||||
<span v-else-if="props.column.label == 'Active'">
|
||||
{{ props.column.label }}
|
||||
<span
|
||||
class="tooltip outline-none"
|
||||
data-tippy-content="When an alias is deactivated, any messages sent to it will be silently discarded. The sender will not be notified of the unsuccessful delivery."
|
||||
>
|
||||
<icon name="info" class="inline-block w-4 h-4 text-grey-300 fill-current" />
|
||||
</span>
|
||||
</span>
|
||||
<span v-else :class="selectedRows.length > 0 ? 'blur-sm' : ''">
|
||||
{{ props.column.label }}
|
||||
</span>
|
||||
|
@ -1153,7 +1162,7 @@
|
|||
</p>
|
||||
<p class="mt-4 text-grey-700">
|
||||
To send from an alias you must send the email from a <b>verified recipient</b> on your
|
||||
addy.io account.
|
||||
account.
|
||||
</p>
|
||||
<label for="send_from_alias" class="block font-medium leading-6 text-grey-600 text-sm my-2">
|
||||
Alias to send from
|
||||
|
@ -1252,6 +1261,38 @@
|
|||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal :open="newAliasModalOpen" @close="newAliasModalOpen = false">
|
||||
<template v-slot:title> Your New Alias is: </template>
|
||||
<template v-slot:content>
|
||||
<p class="my-8 text-grey-700">
|
||||
<button
|
||||
class="text-grey-400 tooltip outline-none"
|
||||
data-tippy-content="Click to copy"
|
||||
@click="clipboard(getNewAliasEmail())"
|
||||
>
|
||||
<span class="font-semibold text-indigo-800">{{
|
||||
newAliasExtension ? `${newAliasLocalPart}+${newAliasExtension}` : newAliasLocalPart
|
||||
}}</span
|
||||
><span class="font-semibold text-grey-500">@{{ newAliasDomain }}</span>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
|
||||
<button
|
||||
@click="clipboard(getNewAliasEmail())"
|
||||
class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
@click="newAliasModalOpen = false"
|
||||
class="px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal :open="moreInfoOpen" @close="moreInfoOpen = false">
|
||||
<template v-slot:title> More information </template>
|
||||
<template v-slot:content>
|
||||
|
@ -1407,6 +1448,10 @@ const restoreAliasModalOpen = ref(false)
|
|||
const editAliasRecipientsLoading = ref(false)
|
||||
const editAliasRecipientsModalOpen = ref(false)
|
||||
const createAliasModalOpen = ref(false)
|
||||
const newAliasLocalPart = ref('')
|
||||
const newAliasExtension = ref('')
|
||||
const newAliasDomain = ref('')
|
||||
const newAliasModalOpen = ref(false)
|
||||
const createAliasLoading = ref(false)
|
||||
const createAliasDomain = ref(props.defaultAliasDomain)
|
||||
const createAliasLocalPart = ref('')
|
||||
|
@ -1530,6 +1575,10 @@ const sortOptions = [
|
|||
value: 'last_sent',
|
||||
label: 'Last Sent At',
|
||||
},
|
||||
{
|
||||
value: 'last_used',
|
||||
label: 'Last Used At',
|
||||
},
|
||||
{
|
||||
value: 'updated_at',
|
||||
label: 'Updated At',
|
||||
|
@ -1656,14 +1705,19 @@ const createNewAlias = () => {
|
|||
)
|
||||
.then(({ data }) => {
|
||||
// Show active/inactive
|
||||
router.visit(route('aliases.index'), {
|
||||
router.reload({
|
||||
only: ['initialRows', 'search', 'currentAliasStatus', 'sort', 'sortDirection'],
|
||||
onSuccess: page => {
|
||||
rows.value = page.props.initialRows.data
|
||||
createAliasLoading.value = false
|
||||
createAliasLocalPart.value = ''
|
||||
createAliasDescription.value = ''
|
||||
createAliasRecipientIds.value = []
|
||||
createAliasModalOpen.value = false
|
||||
newAliasLocalPart.value = data.data.local_part
|
||||
newAliasExtension.value = data.data.extension
|
||||
newAliasDomain.value = data.data.domain
|
||||
newAliasModalOpen.value = true
|
||||
successMessage('New alias created successfully')
|
||||
},
|
||||
})
|
||||
|
@ -2298,6 +2352,12 @@ const getAliasEmail = alias => {
|
|||
return alias.extension ? `${alias.local_part}+${alias.extension}@${alias.domain}` : alias.email
|
||||
}
|
||||
|
||||
const getNewAliasEmail = () => {
|
||||
return newAliasExtension.value
|
||||
? `${newAliasLocalPart.value}+${newAliasExtension.value}@${newAliasDomain.value}`
|
||||
: `${newAliasLocalPart.value}@${newAliasDomain.value}`
|
||||
}
|
||||
|
||||
const getAliasLocalPart = alias => {
|
||||
return alias.extension ? `${alias.local_part}+${alias.extension}` : alias.local_part
|
||||
}
|
||||
|
|
|
@ -165,6 +165,178 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-8">
|
||||
<div class="block text-lg font-medium text-grey-700">Alias Auto Create Regex</div>
|
||||
<p class="mt-1 text-base text-grey-700">
|
||||
If you wish to create aliases on-the-fly but don't want to enable catch-all then you can
|
||||
enter a regular expression pattern below. If a new alias' local part matches the pattern
|
||||
then it will still be created on-the-fly even though catch-all is disabled.
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
Note: <b>Catch-All must be disabled</b> to use alias automatic creation with regex.
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
For example, if you only want aliases that start with "prefix" to be automatically
|
||||
created, use the regex <span class="bg-cyan-200 px-1 rounded-md">^prefix</span>
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
If you only want aliases that end with "suffix" to be automatically created, use the
|
||||
regex <span class="bg-cyan-200 px-1 rounded-md">suffix$</span>
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
If you want to make sure the local part is fully matched you can start your regex with
|
||||
<span class="bg-cyan-200 px-1 rounded-md">^</span> and end it with
|
||||
<span class="bg-cyan-200 px-1 rounded-md">$</span> e.g.
|
||||
<span class="bg-cyan-200 px-1 rounded-md">^prefix.*suffix$</span> which would match
|
||||
"prefix-anything-here-suffix"
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
You can use
|
||||
<a
|
||||
href="https://regex101.com/"
|
||||
class="text-indigo-800"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
>regex101.com</a
|
||||
>
|
||||
to help you write your regular expressions.
|
||||
</p>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="mt-6 grid grid-cols-1 mb-4">
|
||||
<label
|
||||
for="auto_create_regex"
|
||||
class="block text-sm font-medium leading-6 text-grey-900"
|
||||
>Auto Create Regex</label
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<input
|
||||
v-model="domain.auto_create_regex"
|
||||
type="text"
|
||||
name="auto_create_regex"
|
||||
id="auto_create_regex"
|
||||
class="block w-full rounded-md border-0 py-2 pr-10 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-base sm:leading-6"
|
||||
:class="
|
||||
errors.auto_create_regex
|
||||
? 'text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500'
|
||||
: 'text-grey-900 ring-grey-300 placeholder:text-grey-400 focus:ring-indigo-600'
|
||||
"
|
||||
placeholder="^prefix"
|
||||
aria-invalid="true"
|
||||
aria-describedby="auto-create-regex-error"
|
||||
/>
|
||||
<div
|
||||
v-if="errors.auto_create_regex"
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<ExclamationCircleIcon class="h-5 w-5 text-red-500" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="errors.auto_create_regex"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
id="auto-create-regex-error"
|
||||
>
|
||||
{{ errors.auto_create_regex }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="editAutoCreateRegex"
|
||||
:disabled="domain.autoCreateRegexLoading"
|
||||
class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
|
||||
>
|
||||
Update Auto Create Regex
|
||||
<loader v-if="domain.autoCreateRegexLoading" />
|
||||
</button>
|
||||
|
||||
<div class="block text-lg font-medium text-grey-700 pt-8">
|
||||
Test Alias Auto Create Regex
|
||||
</div>
|
||||
<p class="mt-1 text-base text-grey-700">
|
||||
You can test whether an alias local part will match the above regex pattern and be
|
||||
automatically created by entering the local part (left of @ symbol) below.
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">No aliases will be created when testing.</p>
|
||||
<div class="mb-6">
|
||||
<div class="mt-6 grid grid-cols-1 mb-4">
|
||||
<label
|
||||
for="auto_create_regex"
|
||||
class="block text-sm font-medium leading-6 text-grey-900"
|
||||
>Alias Local Part</label
|
||||
>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="flex">
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
v-model="domain.test_auto_create_regex_local_part"
|
||||
type="text"
|
||||
name="test_auto_create_regex_local_part"
|
||||
id="test_auto_create_regex_local_part"
|
||||
class="block w-full min-w-0 flex-1 rounded-none rounded-l-md border-0 py-2 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6"
|
||||
:class="testAutoCreateRegexLocalPartClass"
|
||||
placeholder="local-part"
|
||||
aria-invalid="true"
|
||||
aria-describedby="test-auto-create-regex-local-part-error"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
errors.test_auto_create_regex_local_part ||
|
||||
domain.testAutoCreateRegexSuccess === false
|
||||
"
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<ExclamationCircleIcon class="h-5 w-5 text-red-500" aria-hidden="true" />
|
||||
</div>
|
||||
<div
|
||||
v-if="domain.testAutoCreateRegexSuccess === true"
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<CheckCircleIcon class="h-5 w-5 text-green-500" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center rounded-r-md border border-l-0 border-grey-300 px-3 text-grey-500 sm:text-sm"
|
||||
>@{{ domain.domain }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="errors.test_auto_create_regex_local_part"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
id="test-auto-create-regex-local-part-error"
|
||||
>
|
||||
{{ errors.test_auto_create_regex_local_part }}
|
||||
</p>
|
||||
<p
|
||||
v-if="domain.testAutoCreateRegexSuccess === false"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
id="test-auto-create-regex-local-part-error"
|
||||
>
|
||||
The alias local part does not match the regular expression and would not be created
|
||||
</p>
|
||||
<p
|
||||
v-if="domain.testAutoCreateRegexSuccess === true"
|
||||
class="mt-2 text-sm text-green-600"
|
||||
id="test-auto-create-regex-local-part-error"
|
||||
>
|
||||
The alias local part matches the regular expression and would be created
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="testAutoCreateRegex"
|
||||
:disabled="domain.testAutoCreateRegexLoading"
|
||||
class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
|
||||
>
|
||||
Test Auto Create Regex
|
||||
<loader v-if="domain.testAutoCreateRegexLoading" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<span
|
||||
class="mt-2 text-sm text-grey-500 tooltip"
|
||||
|
@ -178,12 +350,12 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Head, Link } from '@inertiajs/vue3'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { Head } from '@inertiajs/vue3'
|
||||
import { notify } from '@kyvg/vue3-notification'
|
||||
import { roundArrow } from 'tippy.js'
|
||||
import tippy from 'tippy.js'
|
||||
import { ExclamationCircleIcon } from '@heroicons/vue/20/solid'
|
||||
import { ExclamationCircleIcon, CheckCircleIcon } from '@heroicons/vue/20/solid'
|
||||
|
||||
const props = defineProps({
|
||||
initialDomain: {
|
||||
|
@ -202,6 +374,21 @@ onMounted(() => {
|
|||
addTooltips()
|
||||
})
|
||||
|
||||
const testAutoCreateRegexLocalPartClass = computed(() => {
|
||||
if (
|
||||
errors.value.test_auto_create_regex_local_part ||
|
||||
domain.value.testAutoCreateRegexSuccess === false
|
||||
) {
|
||||
return 'text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500'
|
||||
}
|
||||
|
||||
if (domain.value.testAutoCreateRegexSuccess === true) {
|
||||
return 'text-green-900 ring-green-300 placeholder:text-green-300 focus:ring-green-500'
|
||||
}
|
||||
|
||||
return 'text-grey-900 ring-grey-300 placeholder:text-grey-400 focus:ring-indigo-600'
|
||||
})
|
||||
|
||||
const editFromName = () => {
|
||||
errors.value = {}
|
||||
|
||||
|
@ -232,6 +419,94 @@ const editFromName = () => {
|
|||
})
|
||||
}
|
||||
|
||||
const editAutoCreateRegex = () => {
|
||||
errors.value = {}
|
||||
|
||||
if (domain.value.auto_create_regex !== null && domain.value.auto_create_regex.length > 100) {
|
||||
errors.value.auto_create_regex = "'Auto Create Regex' cannot be more than 100 characters"
|
||||
return errorMessage(errors.value.auto_create_regex)
|
||||
}
|
||||
|
||||
domain.value.autoCreateRegexLoading = true
|
||||
|
||||
axios
|
||||
.patch(
|
||||
`/api/v1/domains/${domain.value.id}`,
|
||||
JSON.stringify({
|
||||
auto_create_regex: domain.value.auto_create_regex,
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
)
|
||||
.then(response => {
|
||||
domain.value.autoCreateRegexLoading = false
|
||||
successMessage("Domain 'Auto Create Regex' updated")
|
||||
})
|
||||
.catch(error => {
|
||||
domain.value.autoCreateRegexLoading = false
|
||||
|
||||
if (error.response.data.message !== undefined) {
|
||||
errors.value.auto_create_regex = error.response.data.message
|
||||
errorMessage(error.response.data.message)
|
||||
} else {
|
||||
errorMessage()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const testAutoCreateRegex = () => {
|
||||
domain.value.testAutoCreateRegexSuccess = null
|
||||
errors.value = {}
|
||||
|
||||
if (domain.value.auto_create_regex === null) {
|
||||
return (errors.value.test_auto_create_regex_local_part =
|
||||
'You must first enter a regex pattern above')
|
||||
}
|
||||
|
||||
// Validate alias local part
|
||||
if (
|
||||
domain.value.test_auto_create_regex_local_part !== null &&
|
||||
!validLocalPart(domain.value.test_auto_create_regex_local_part)
|
||||
) {
|
||||
errors.value.test_auto_create_regex_local_part = "Invalid 'Alias Local Part'"
|
||||
return errorMessage(errors.value.test_auto_create_regex_local_part)
|
||||
}
|
||||
|
||||
domain.value.testAutoCreateRegexLoading = true
|
||||
|
||||
axios
|
||||
.post(
|
||||
'/test-auto-create-regex',
|
||||
JSON.stringify({
|
||||
resource: 'domain',
|
||||
local_part: domain.value.test_auto_create_regex_local_part,
|
||||
id: domain.value.id,
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
)
|
||||
.then(response => {
|
||||
domain.value.testAutoCreateRegexLoading = false
|
||||
|
||||
if (response.data.success) {
|
||||
domain.value.testAutoCreateRegexSuccess = true
|
||||
} else {
|
||||
domain.value.testAutoCreateRegexSuccess = false
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
domain.value.testAutoCreateRegexLoading = false
|
||||
if (error.response.data.message !== undefined) {
|
||||
errors.value.test_auto_create_regex_local_part = error.response.data.message
|
||||
errorMessage(error.response.data.message)
|
||||
} else {
|
||||
errorMessage()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addTooltips = () => {
|
||||
if (tippyInstance.value) {
|
||||
_.each(tippyInstance.value, instance => instance.destroy())
|
||||
|
@ -255,6 +530,11 @@ const clipboard = (str, success, error) => {
|
|||
)
|
||||
}
|
||||
|
||||
const validLocalPart = part => {
|
||||
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))$/
|
||||
return re.test(part)
|
||||
}
|
||||
|
||||
const successMessage = (text = '') => {
|
||||
notify({
|
||||
title: 'Success',
|
||||
|
|
|
@ -38,6 +38,29 @@
|
|||
}"
|
||||
styleClass="vgt-table"
|
||||
>
|
||||
<template #table-column="props">
|
||||
<span v-if="props.column.label == 'Active'">
|
||||
{{ props.column.label }}
|
||||
<span
|
||||
class="tooltip outline-none"
|
||||
data-tippy-content="When a domain is deactivated, any messages sent to its aliases will be silently discarded. The sender will not be notified of the unsuccessful delivery."
|
||||
>
|
||||
<icon name="info" class="inline-block w-4 h-4 text-grey-300 fill-current" />
|
||||
</span>
|
||||
</span>
|
||||
<span v-else-if="props.column.label == 'Catch-All'">
|
||||
{{ props.column.label }}
|
||||
<span
|
||||
class="tooltip outline-none"
|
||||
data-tippy-content="When catch-all is disabled, only aliases that already exist for the domain will forward messages. They will not be automatically created on-the-fly unless you are using auto create regex."
|
||||
>
|
||||
<icon name="info" class="inline-block w-4 h-4 text-grey-300 fill-current" />
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ props.column.label }}
|
||||
</span>
|
||||
</template>
|
||||
<template #table-row="props">
|
||||
<span
|
||||
v-if="props.column.field == 'created_at'"
|
||||
|
@ -677,12 +700,14 @@ const columns = [
|
|||
label: 'Active',
|
||||
field: 'active',
|
||||
type: 'boolean',
|
||||
sortable: false,
|
||||
globalSearchDisabled: true,
|
||||
},
|
||||
{
|
||||
label: 'Catch-All',
|
||||
field: 'catch_all',
|
||||
type: 'boolean',
|
||||
sortable: false,
|
||||
globalSearchDisabled: true,
|
||||
},
|
||||
{
|
||||
|
@ -969,7 +994,7 @@ const closeCheckRecordsModal = () => {
|
|||
}
|
||||
|
||||
const validDomain = domain => {
|
||||
let re = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/
|
||||
let re = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z0-9-]{2,63}$)/
|
||||
return re.test(domain)
|
||||
}
|
||||
|
||||
|
@ -978,7 +1003,7 @@ const validateNewDomain = e => {
|
|||
|
||||
if (!newDomain.value) {
|
||||
errors.value.newDomain = 'Domain name required'
|
||||
} else if (newDomain.value.length > 50) {
|
||||
} else if (newDomain.value.length > 100) {
|
||||
errors.value.newDomain = 'That domain name is too long'
|
||||
} else if (!validDomain(newDomain.value)) {
|
||||
errors.value.newDomain = 'Please enter a valid domain name'
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
>
|
||||
<template #table-column="props">
|
||||
<span v-if="props.column.label == 'Key'">
|
||||
Key
|
||||
{{ props.column.label }}
|
||||
<span
|
||||
class="tooltip outline-none"
|
||||
:data-tippy-content="`Use this to attach recipients to new aliases as they are created e.g. alias+key@${$page.props.user.username}.anonaddy.com. You can attach multiple recipients by doing alias+2.3.4@${$page.props.user.username}.anonaddy.com. Separating each key by a full stop.`"
|
||||
|
@ -49,7 +49,7 @@
|
|||
</span>
|
||||
</span>
|
||||
<span v-else-if="props.column.label == 'Alias Count'">
|
||||
Alias Count
|
||||
{{ props.column.label }}
|
||||
<span
|
||||
class="tooltip outline-none"
|
||||
data-tippy-content="This shows the total number of aliases that either the recipient is directly assigned to, or where the recipient is set as the default for a custom domain or username."
|
||||
|
|
|
@ -236,6 +236,7 @@
|
|||
<div class="relative sm:mr-4">
|
||||
<select
|
||||
v-model="createRuleObject.conditions[key].match"
|
||||
@change="ruleConditionMatchChange(createRuleObject.conditions[key])"
|
||||
:id="`create_rule_condition_matches_${key}`"
|
||||
class="block appearance-none w-full sm:w-40 text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
|
||||
required
|
||||
|
@ -561,6 +562,7 @@
|
|||
<div class="relative sm:mr-4">
|
||||
<select
|
||||
v-model="editRuleObject.conditions[key].match"
|
||||
@change="ruleConditionMatchChange(editRuleObject.conditions[key])"
|
||||
:id="`edit_rule_condition_matches_${key}`"
|
||||
class="block appearance-none w-full sm:w-40 text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
|
||||
required
|
||||
|
@ -1251,6 +1253,8 @@ const conditionMatchOptions = (object, key) => {
|
|||
'does not start with',
|
||||
'ends with',
|
||||
'does not end with',
|
||||
'matches regex',
|
||||
'does not match regex',
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1274,14 +1278,25 @@ const deleteCondition = (object, key) => {
|
|||
}
|
||||
|
||||
const addValueToCondition = (object, key) => {
|
||||
if (!object.conditions[key].currentConditionValue) {
|
||||
return (errors.value.ruleConditions = `You must enter a value to insert`)
|
||||
}
|
||||
|
||||
if (object.conditions[key].values.length >= 10) {
|
||||
return (errors.value.ruleConditions = `You cannot add more than 10 values per condition`)
|
||||
}
|
||||
|
||||
if (object.conditions[key].currentConditionValue) {
|
||||
object.conditions[key].values.push(object.conditions[key].currentConditionValue)
|
||||
if (['matches regex', 'does not match regex'].includes(object.conditions[key].match)) {
|
||||
try {
|
||||
let re = new RegExp(object.conditions[key].currentConditionValue)
|
||||
re.test('')
|
||||
} catch (e) {
|
||||
return (errors.value.ruleConditions = `Please enter a valid regular expression`)
|
||||
}
|
||||
}
|
||||
|
||||
object.conditions[key].values.push(object.conditions[key].currentConditionValue)
|
||||
|
||||
// Reset current conditon value input
|
||||
object.conditions[key].currentConditionValue = ''
|
||||
}
|
||||
|
@ -1324,6 +1339,10 @@ const resetCreateRuleObject = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const ruleConditionMatchChange = condition => {
|
||||
errors.value.ruleConditions = ''
|
||||
}
|
||||
|
||||
const ruleActionChange = action => {
|
||||
if (action.type === 'subject' || action.type === 'displayFrom' || action.type === 'select') {
|
||||
action.value = ''
|
||||
|
|
|
@ -21,13 +21,21 @@
|
|||
class="text-indigo-700"
|
||||
>Firefox</a
|
||||
>
|
||||
or
|
||||
,
|
||||
<a
|
||||
href="https://chrome.google.com/webstore/detail/addyio-anonymous-email-fo/iadbdpnoknmbdeolbapdackdcogdmjpe"
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
class="text-indigo-700"
|
||||
>Chrome / Brave</a
|
||||
>Chrome, Brave</a
|
||||
>
|
||||
or
|
||||
<a
|
||||
href="https://microsoftedge.microsoft.com/addons/detail/addyio-anonymous-email/ohjlgpcfncgkijjfmabldlgnccmgcehl"
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
class="text-indigo-700"
|
||||
>Edge</a
|
||||
>
|
||||
to create new aliases. They can also be used with the mobile apps. Simply paste a key
|
||||
you've created into the browser extension or mobile apps to get started. Your API access
|
||||
|
@ -197,7 +205,7 @@
|
|||
<div class="text-center">
|
||||
<img :src="qrCode" class="inline-block" alt="QR Code" />
|
||||
<p class="text-left text-sm text-grey-700">
|
||||
You can scan this QR code to automatically login to the addy.io mobile app by Stjin.
|
||||
You can scan this QR code to automatically login to the addy.io mobile app.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
|
|
|
@ -721,7 +721,7 @@
|
|||
<div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
|
||||
<button
|
||||
@click="disableKey"
|
||||
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disable:cursor-not-allowed"
|
||||
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
|
||||
:disabled="disableKeyLoading"
|
||||
>
|
||||
Disable
|
||||
|
@ -771,7 +771,7 @@
|
|||
<div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
|
||||
<button
|
||||
@click="deleteKey"
|
||||
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disable:cursor-not-allowed"
|
||||
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
|
||||
:disabled="deleteKeyLoading"
|
||||
>
|
||||
Remove
|
||||
|
|
|
@ -125,6 +125,177 @@
|
|||
@off="disallowLogin()"
|
||||
/>
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<div class="block text-lg font-medium text-grey-700">Alias Auto Create Regex</div>
|
||||
<p class="mt-1 text-base text-grey-700">
|
||||
If you wish to create aliases on-the-fly but don't want to enable catch-all then you can
|
||||
enter a regular expression pattern below. If a new alias' local part matches the pattern
|
||||
then it will still be created on-the-fly even though catch-all is disabled.
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
Note: <b>Catch-All must be disabled</b> to use alias automatic creation with regex.
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
For example, if you only want aliases that start with "prefix" to be automatically
|
||||
created, use the regex <span class="bg-cyan-200 px-1 rounded-md">^prefix</span>
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
If you only want aliases that end with "suffix" to be automatically created, use the
|
||||
regex <span class="bg-cyan-200 px-1 rounded-md">suffix$</span>
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
If you want to make sure the local part is fully matched you can start your regex with
|
||||
<span class="bg-cyan-200 px-1 rounded-md">^</span> and end it with
|
||||
<span class="bg-cyan-200 px-1 rounded-md">$</span> e.g.
|
||||
<span class="bg-cyan-200 px-1 rounded-md">^prefix.*suffix$</span> which would match
|
||||
"prefix-anything-here-suffix"
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">
|
||||
You can use
|
||||
<a
|
||||
href="https://regex101.com/"
|
||||
class="text-indigo-800"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
>regex101.com</a
|
||||
>
|
||||
to help you write your regular expressions.
|
||||
</p>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="mt-6 grid grid-cols-1 mb-4">
|
||||
<label
|
||||
for="auto_create_regex"
|
||||
class="block text-sm font-medium leading-6 text-grey-900"
|
||||
>Auto Create Regex</label
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<input
|
||||
v-model="username.auto_create_regex"
|
||||
type="text"
|
||||
name="auto_create_regex"
|
||||
id="auto_create_regex"
|
||||
class="block w-full rounded-md border-0 py-2 pr-10 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-base sm:leading-6"
|
||||
:class="
|
||||
errors.auto_create_regex
|
||||
? 'text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500'
|
||||
: 'text-grey-900 ring-grey-300 placeholder:text-grey-400 focus:ring-indigo-600'
|
||||
"
|
||||
placeholder="^prefix"
|
||||
aria-invalid="true"
|
||||
aria-describedby="auto-create-regex-error"
|
||||
/>
|
||||
<div
|
||||
v-if="errors.auto_create_regex"
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<ExclamationCircleIcon class="h-5 w-5 text-red-500" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="errors.auto_create_regex"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
id="auto-create-regex-error"
|
||||
>
|
||||
{{ errors.auto_create_regex }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="editAutoCreateRegex"
|
||||
:disabled="username.autoCreateRegexLoading"
|
||||
class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
|
||||
>
|
||||
Update Auto Create Regex
|
||||
<loader v-if="username.autoCreateRegexLoading" />
|
||||
</button>
|
||||
|
||||
<div class="block text-lg font-medium text-grey-700 pt-8">
|
||||
Test Alias Auto Create Regex
|
||||
</div>
|
||||
<p class="mt-1 text-base text-grey-700">
|
||||
You can test whether an alias local part will match the above regex pattern and be
|
||||
automatically created by entering the local part (left of @ symbol) below.
|
||||
</p>
|
||||
<p class="mt-2 text-base text-grey-700">No aliases will be created when testing.</p>
|
||||
<div class="mb-6">
|
||||
<div class="mt-6 grid grid-cols-1 mb-4">
|
||||
<label
|
||||
for="auto_create_regex"
|
||||
class="block text-sm font-medium leading-6 text-grey-900"
|
||||
>Alias Local Part</label
|
||||
>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="flex">
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
v-model="username.test_auto_create_regex_local_part"
|
||||
type="text"
|
||||
name="test_auto_create_regex_local_part"
|
||||
id="test_auto_create_regex_local_part"
|
||||
class="block w-full min-w-0 flex-1 rounded-none rounded-l-md border-0 py-2 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6"
|
||||
:class="testAutoCreateRegexLocalPartClass"
|
||||
placeholder="local-part"
|
||||
aria-invalid="true"
|
||||
aria-describedby="test-auto-create-regex-local-part-error"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
errors.test_auto_create_regex_local_part ||
|
||||
username.testAutoCreateRegexSuccess === false
|
||||
"
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<ExclamationCircleIcon class="h-5 w-5 text-red-500" aria-hidden="true" />
|
||||
</div>
|
||||
<div
|
||||
v-if="username.testAutoCreateRegexSuccess === true"
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<CheckCircleIcon class="h-5 w-5 text-green-500" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center rounded-r-md border border-l-0 border-grey-300 px-3 text-grey-500 sm:text-sm"
|
||||
>@{{ username.username }}.anonaddy.com</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="errors.test_auto_create_regex_local_part"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
id="test-auto-create-regex-local-part-error"
|
||||
>
|
||||
{{ errors.test_auto_create_regex_local_part }}
|
||||
</p>
|
||||
<p
|
||||
v-if="username.testAutoCreateRegexSuccess === false"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
id="test-auto-create-regex-local-part-error"
|
||||
>
|
||||
The alias local part does not match the regular expression and would not be created
|
||||
</p>
|
||||
<p
|
||||
v-if="username.testAutoCreateRegexSuccess === true"
|
||||
class="mt-2 text-sm text-green-600"
|
||||
id="test-auto-create-regex-local-part-error"
|
||||
>
|
||||
The alias local part matches the regular expression and would be created
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="testAutoCreateRegex"
|
||||
:disabled="username.testAutoCreateRegexLoading"
|
||||
class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
|
||||
>
|
||||
Test Auto Create Regex
|
||||
<loader v-if="username.testAutoCreateRegexLoading" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<span
|
||||
|
@ -139,12 +310,12 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Head, usePage, Link } from '@inertiajs/vue3'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { Head, usePage } from '@inertiajs/vue3'
|
||||
import { notify } from '@kyvg/vue3-notification'
|
||||
import { roundArrow } from 'tippy.js'
|
||||
import tippy from 'tippy.js'
|
||||
import { ExclamationCircleIcon } from '@heroicons/vue/20/solid'
|
||||
import { ExclamationCircleIcon, CheckCircleIcon } from '@heroicons/vue/20/solid'
|
||||
import Toggle from '../../Components/Toggle.vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -165,6 +336,21 @@ onMounted(() => {
|
|||
addTooltips()
|
||||
})
|
||||
|
||||
const testAutoCreateRegexLocalPartClass = computed(() => {
|
||||
if (
|
||||
errors.value.test_auto_create_regex_local_part ||
|
||||
username.value.testAutoCreateRegexSuccess === false
|
||||
) {
|
||||
return 'text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500'
|
||||
}
|
||||
|
||||
if (username.value.testAutoCreateRegexSuccess === true) {
|
||||
return 'text-green-900 ring-green-300 placeholder:text-green-300 focus:ring-green-500'
|
||||
}
|
||||
|
||||
return 'text-grey-900 ring-grey-300 placeholder:text-grey-400 focus:ring-indigo-600'
|
||||
})
|
||||
|
||||
const editFromName = () => {
|
||||
errors.value = {}
|
||||
|
||||
|
@ -225,6 +411,94 @@ const disallowLogin = () => {
|
|||
})
|
||||
}
|
||||
|
||||
const editAutoCreateRegex = () => {
|
||||
errors.value = {}
|
||||
|
||||
if (username.value.auto_create_regex !== null && username.value.auto_create_regex.length > 100) {
|
||||
errors.value.auto_create_regex = "'Auto Create Regex' cannot be more than 100 characters"
|
||||
return errorMessage(errors.value.auto_create_regex)
|
||||
}
|
||||
|
||||
username.value.autoCreateRegexLoading = true
|
||||
|
||||
axios
|
||||
.patch(
|
||||
`/api/v1/usernames/${username.value.id}`,
|
||||
JSON.stringify({
|
||||
auto_create_regex: username.value.auto_create_regex,
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
)
|
||||
.then(response => {
|
||||
username.value.autoCreateRegexLoading = false
|
||||
successMessage("Username 'Auto Create Regex' updated")
|
||||
})
|
||||
.catch(error => {
|
||||
username.value.autoCreateRegexLoading = false
|
||||
|
||||
if (error.response.data.message !== undefined) {
|
||||
errors.value.auto_create_regex = error.response.data.message
|
||||
errorMessage(error.response.data.message)
|
||||
} else {
|
||||
errorMessage()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const testAutoCreateRegex = () => {
|
||||
username.value.testAutoCreateRegexSuccess = null
|
||||
errors.value = {}
|
||||
|
||||
if (username.value.auto_create_regex === null) {
|
||||
return (errors.value.test_auto_create_regex_local_part =
|
||||
'You must first enter a regex pattern above')
|
||||
}
|
||||
|
||||
// Validate alias local part
|
||||
if (
|
||||
username.value.test_auto_create_regex_local_part !== null &&
|
||||
!validLocalPart(username.value.test_auto_create_regex_local_part)
|
||||
) {
|
||||
errors.value.test_auto_create_regex_local_part = "Invalid 'Alias Local Part'"
|
||||
return errorMessage(errors.value.test_auto_create_regex_local_part)
|
||||
}
|
||||
|
||||
username.value.testAutoCreateRegexLoading = true
|
||||
|
||||
axios
|
||||
.post(
|
||||
'/test-auto-create-regex',
|
||||
JSON.stringify({
|
||||
resource: 'username',
|
||||
local_part: username.value.test_auto_create_regex_local_part,
|
||||
id: username.value.id,
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
)
|
||||
.then(response => {
|
||||
username.value.testAutoCreateRegexLoading = false
|
||||
|
||||
if (response.data.success) {
|
||||
username.value.testAutoCreateRegexSuccess = true
|
||||
} else {
|
||||
username.value.testAutoCreateRegexSuccess = false
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
username.value.testAutoCreateRegexLoading = false
|
||||
if (error.response.data.message !== undefined) {
|
||||
errors.value.test_auto_create_regex_local_part = error.response.data.message
|
||||
errorMessage(error.response.data.message)
|
||||
} else {
|
||||
errorMessage()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addTooltips = () => {
|
||||
if (tippyInstance.value) {
|
||||
_.each(tippyInstance.value, instance => instance.destroy())
|
||||
|
@ -252,6 +526,11 @@ const clipboard = (str, success, error) => {
|
|||
)
|
||||
}
|
||||
|
||||
const validLocalPart = part => {
|
||||
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))$/
|
||||
return re.test(part)
|
||||
}
|
||||
|
||||
const successMessage = (text = '') => {
|
||||
notify({
|
||||
title: 'Success',
|
||||
|
|
|
@ -38,6 +38,29 @@
|
|||
}"
|
||||
styleClass="vgt-table"
|
||||
>
|
||||
<template #table-column="props">
|
||||
<span v-if="props.column.label == 'Active'">
|
||||
{{ props.column.label }}
|
||||
<span
|
||||
class="tooltip outline-none"
|
||||
data-tippy-content="When a username is deactivated, any messages sent to its aliases will be silently discarded. The sender will not be notified of the unsuccessful delivery."
|
||||
>
|
||||
<icon name="info" class="inline-block w-4 h-4 text-grey-300 fill-current" />
|
||||
</span>
|
||||
</span>
|
||||
<span v-else-if="props.column.label == 'Catch-All'">
|
||||
{{ props.column.label }}
|
||||
<span
|
||||
class="tooltip outline-none"
|
||||
data-tippy-content="When catch-all is disabled, only aliases that already exist for the username will forward messages. They will not be automatically created on-the-fly unless you are using auto create regex."
|
||||
>
|
||||
<icon name="info" class="inline-block w-4 h-4 text-grey-300 fill-current" />
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ props.column.label }}
|
||||
</span>
|
||||
</template>
|
||||
<template #table-row="props">
|
||||
<span
|
||||
v-if="props.column.field == 'created_at'"
|
||||
|
@ -445,12 +468,14 @@ const columns = [
|
|||
label: 'Active',
|
||||
field: 'active',
|
||||
type: 'boolean',
|
||||
sortable: false,
|
||||
globalSearchDisabled: true,
|
||||
},
|
||||
{
|
||||
label: 'Catch-All',
|
||||
field: 'catch_all',
|
||||
type: 'boolean',
|
||||
sortable: false,
|
||||
globalSearchDisabled: true,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Domain MX records invalid
|
||||
|
||||
A recent DNS record check on your custom domain **{{ $domain }}** on addy.io showed that your MX records are no longer pointing to the addy.io server. This means that addy.io will not be able to handle your emails for you.
|
||||
A recent DNS record check on your custom domain **{{ $domain }}** on {{ config('app.name') }} showed that your MX records are no longer pointing to the {{ config('app.name') }} server. This means that {{ config('app.name') }} will not be able to handle your emails for you.
|
||||
|
||||
If this MX record change was intentional then you can ignore this email.
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Domain Unverified For Sending
|
||||
|
||||
A recent DNS record check on your custom domain **{{ $domain }}** failed on addy.io. This means that your domain had been unverified for sending until the DNS records are added correctly.
|
||||
A recent DNS record check on your custom domain **{{ $domain }}** failed on {{ config('app.name') }}. This means that your domain had been unverified for sending until the DNS records are added correctly.
|
||||
|
||||
The check failed for the following reason:
|
||||
|
||||
|
@ -10,7 +10,7 @@ The check failed for the following reason:
|
|||
|
||||
Please visit the domains page on the site by clicking the button below to resolve the issue.
|
||||
|
||||
Emails for your custom domain will be sent from an addy.io domain in the mean time.
|
||||
Emails for your custom domain will be sent from an {{ config('app.name') }} domain in the mean time.
|
||||
|
||||
@component('mail::button', ['url' => config('app.url').'/domains'])
|
||||
Check Domain
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Failed two factor authentication login attempt
|
||||
|
||||
Someone just entered an incorrect OTP while trying to login to your addy.io account. The username (**{{ $username }}**) and password were correct.
|
||||
Someone just entered an incorrect OTP while trying to login to your {{ config('app.name') }} account. The username (**{{ $username }}**) and password were correct.
|
||||
|
||||
The login has been blocked. If this was you, then you can ignore this notification.
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
# Your API key expires soon
|
||||
|
||||
@if($tokenName)
|
||||
Your API key named "**{{ $tokenName }}**" on your addy.io account expires in **one weeks time**.
|
||||
Your API key named "**{{ $tokenName }}**" on your {{ config('app.name') }} account expires in **one weeks time**.
|
||||
@else
|
||||
One of the API keys on your addy.io account will expire in **one weeks time**.
|
||||
One of the API keys on your {{ config('app.name') }} account will expire in **one weeks time**.
|
||||
@endif
|
||||
|
||||
If you are not using this API key for the browser extensions, mobile apps or to access the API then you do not need to take any action.
|
||||
|
|
|
@ -28,6 +28,7 @@ use App\Http\Controllers\Api\ReorderRuleController;
|
|||
use App\Http\Controllers\Api\RuleController;
|
||||
use App\Http\Controllers\Api\UsernameController;
|
||||
use App\Http\Controllers\Api\UsernameDefaultRecipientController;
|
||||
use App\Http\Controllers\Auth\ApiAuthenticationController;
|
||||
use App\Http\Controllers\RecipientVerificationController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
|
@ -42,6 +43,12 @@ use Illuminate\Support\Facades\Route;
|
|||
|
|
||||
*/
|
||||
|
||||
// API auth routes for mobile apps and browser extension
|
||||
Route::controller(ApiAuthenticationController::class)->prefix('auth')->group(function () {
|
||||
Route::post('/logout', 'logout');
|
||||
Route::post('/delete-account', 'destroy');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'middleware' => ['auth:sanctum', 'verified'],
|
||||
'prefix' => 'v1',
|
||||
|
|
|
@ -33,6 +33,7 @@ use App\Http\Controllers\ShowRecipientController;
|
|||
use App\Http\Controllers\ShowRuleController;
|
||||
use App\Http\Controllers\ShowUsernameController;
|
||||
use App\Http\Controllers\StoreFailedDeliveryController;
|
||||
use App\Http\Controllers\TestAutoCreateRegexController;
|
||||
use App\Http\Controllers\UseReplyToController;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
@ -50,9 +51,11 @@ use Illuminate\Support\Facades\Route;
|
|||
|
||||
Auth::routes(['verify' => true, 'register' => config('anonaddy.enable_registration')]);
|
||||
|
||||
// Get API access token
|
||||
Route::post('api/auth/login', [ApiAuthenticationController::class, 'login']);
|
||||
Route::post('api/auth/mfa', [ApiAuthenticationController::class, 'mfa']);
|
||||
// API login route needs CSRF middleware so that it can pass it to api/auth/mfa
|
||||
Route::controller(ApiAuthenticationController::class)->prefix('api/auth')->group(function () {
|
||||
Route::post('/login', 'login');
|
||||
Route::post('/mfa', 'mfa');
|
||||
});
|
||||
|
||||
Route::controller(ForgotUsernameController::class)->group(function () {
|
||||
Route::get('/username/reminder', 'show')->name('username.reminder.show');
|
||||
|
@ -122,6 +125,8 @@ Route::middleware(['auth', 'verified', '2fa', 'webauthn'])->group(function () {
|
|||
|
||||
Route::get('/failed-deliveries', [ShowFailedDeliveryController::class, 'index'])->name('failed_deliveries.index');
|
||||
Route::get('/failed-deliveries/{id}/download', [DownloadableFailedDeliveryController::class, 'index'])->name('downloadable_failed_delivery.index');
|
||||
|
||||
Route::post('/test-auto-create-regex', [TestAutoCreateRegexController::class, 'index'])->name('test_auto_create_regex.index');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
|
|
|
@ -441,6 +441,7 @@ class AliasesTest extends TestCase
|
|||
'ids' => [
|
||||
$alias->id,
|
||||
$alias2->id,
|
||||
null,
|
||||
],
|
||||
]);
|
||||
|
||||
|
@ -482,6 +483,7 @@ class AliasesTest extends TestCase
|
|||
'ids' => [
|
||||
$alias->id,
|
||||
$alias2->id,
|
||||
null,
|
||||
],
|
||||
]);
|
||||
|
||||
|
@ -544,6 +546,7 @@ class AliasesTest extends TestCase
|
|||
'ids' => [
|
||||
$alias->id,
|
||||
$alias2->id,
|
||||
null,
|
||||
],
|
||||
]);
|
||||
|
||||
|
@ -575,6 +578,7 @@ class AliasesTest extends TestCase
|
|||
'ids' => [
|
||||
$alias->id,
|
||||
$alias2->id,
|
||||
null,
|
||||
],
|
||||
]);
|
||||
|
||||
|
@ -607,6 +611,7 @@ class AliasesTest extends TestCase
|
|||
'ids' => [
|
||||
$alias->id,
|
||||
$alias2->id,
|
||||
null,
|
||||
],
|
||||
]);
|
||||
|
||||
|
@ -637,6 +642,7 @@ class AliasesTest extends TestCase
|
|||
'ids' => [
|
||||
$alias->id,
|
||||
$alias2->id,
|
||||
null,
|
||||
],
|
||||
]);
|
||||
|
||||
|
@ -669,6 +675,7 @@ class AliasesTest extends TestCase
|
|||
'ids' => [
|
||||
$alias->id,
|
||||
$alias2->id,
|
||||
null,
|
||||
],
|
||||
'recipient_ids' => [
|
||||
$recipient->id,
|
||||
|
@ -683,4 +690,41 @@ class AliasesTest extends TestCase
|
|||
'recipient_id' => $recipient->id,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_cannot_bulk_update_recipients_for_invalid_aliases()
|
||||
{
|
||||
$alias = Alias::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$alias2 = Alias::factory()->create([
|
||||
'user_id' => '00000000-0000-0000-0000-000000000000',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$recipient = Recipient::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->json('POST', '/api/v1/aliases/recipients/bulk', [
|
||||
'ids' => [
|
||||
$alias->id,
|
||||
$alias2->id,
|
||||
null,
|
||||
],
|
||||
'recipient_ids' => [
|
||||
$recipient->id,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertCount(1, $response->getData()->ids);
|
||||
$this->assertEquals('recipients updated for 1 alias successfully', $response->getData()->message);
|
||||
$this->assertDatabaseHas('alias_recipients', [
|
||||
'alias_id' => $alias->id,
|
||||
'recipient_id' => $recipient->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -217,6 +217,37 @@ class DomainsTest extends TestCase
|
|||
$this->assertEquals('John Doe', $response->getData()->data->from_name);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_can_update_domain_auto_create_regex()
|
||||
{
|
||||
$domain = Domain::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->json('PATCH', '/api/v1/domains/'.$domain->id, [
|
||||
'auto_create_regex' => '^prefix',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertEquals('^prefix', $response->getData()->data->auto_create_regex);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function domain_auto_create_regex_must_be_valid()
|
||||
{
|
||||
$domain = Domain::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->json('PATCH', '/api/v1/domains/'.$domain->id, [
|
||||
'auto_create_regex' => '///',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrorFor('auto_create_regex');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_can_delete_domain()
|
||||
{
|
||||
|
|
|
@ -208,7 +208,7 @@ class RecipientsTest extends TestCase
|
|||
#[Test]
|
||||
public function user_can_add_gpg_key_to_recipient()
|
||||
{
|
||||
$gnupg = new \gnupg();
|
||||
$gnupg = new \gnupg;
|
||||
$gnupg->deletekey('26A987650243B28802524E2F809FD0D502E2F695');
|
||||
|
||||
$recipient = Recipient::factory()->create([
|
||||
|
@ -257,7 +257,7 @@ class RecipientsTest extends TestCase
|
|||
#[Test]
|
||||
public function user_can_remove_gpg_key_from_recipient()
|
||||
{
|
||||
$gnupg = new \gnupg();
|
||||
$gnupg = new \gnupg;
|
||||
$gnupg->import(file_get_contents(base_path('tests/keys/AnonAddyPublicKey.asc')));
|
||||
|
||||
$recipient = Recipient::factory()->create([
|
||||
|
|
|
@ -470,7 +470,7 @@ class RulesTest extends TestCase
|
|||
|
||||
protected function getParser($file)
|
||||
{
|
||||
$parser = new Parser();
|
||||
$parser = new Parser;
|
||||
|
||||
// Fix some edge cases in from name e.g. "\" John Doe \"" <johndoe@example.com>
|
||||
$parser->addMiddleware(function ($mimePart, $next) {
|
||||
|
|
|
@ -88,8 +88,8 @@ class UsernamesTest extends TestCase
|
|||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$this->assertEquals(3, $this->user->username_count);
|
||||
$this->assertCount(4, $this->user->usernames);
|
||||
$this->assertEquals(2, $this->user->username_count);
|
||||
$this->assertCount(3, $this->user->usernames);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
@ -294,6 +294,37 @@ class UsernamesTest extends TestCase
|
|||
$this->assertEquals('John Doe', $response->getData()->data->from_name);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_can_update_username_auto_create_regex()
|
||||
{
|
||||
$username = Username::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->json('PATCH', '/api/v1/usernames/'.$username->id, [
|
||||
'auto_create_regex' => '^prefix',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertEquals('^prefix', $response->getData()->data->auto_create_regex);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function username_auto_create_regex_must_be_valid()
|
||||
{
|
||||
$username = Username::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->json('PATCH', '/api/v1/usernames/'.$username->id, [
|
||||
'auto_create_regex' => '///',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrorFor('auto_create_regex');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_can_delete_username()
|
||||
{
|
||||
|
|
|
@ -62,7 +62,7 @@ class ApiAuthenticationTest extends TestCase
|
|||
]);
|
||||
|
||||
$response->assertUnauthorized();
|
||||
$response->assertExactJson(['error' => 'The provided credentials are incorrect']);
|
||||
$response->assertExactJson(['message' => 'The provided credentials are incorrect.']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
@ -120,7 +120,7 @@ class ApiAuthenticationTest extends TestCase
|
|||
]);
|
||||
|
||||
$response->assertForbidden();
|
||||
$response->assertExactJson(['error' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead']);
|
||||
$response->assertExactJson(['message' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead.']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
@ -156,7 +156,7 @@ class ApiAuthenticationTest extends TestCase
|
|||
]);
|
||||
|
||||
$response2->assertUnauthorized();
|
||||
$response2->assertExactJson(['error' => 'The \'One Time Password\' typed was wrong']);
|
||||
$response2->assertExactJson(['message' => 'The \'One Time Password\' typed was wrong.']);
|
||||
|
||||
$response3 = $this->withHeaders([
|
||||
'X-CSRF-TOKEN' => $csrfToken,
|
||||
|
@ -169,4 +169,88 @@ class ApiAuthenticationTest extends TestCase
|
|||
$response3->assertSuccessful();
|
||||
$this->assertEquals($this->user->tokens[0]->token, hash('sha256', $response3->json()['api_key']));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_can_logout_via_api()
|
||||
{
|
||||
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
|
||||
|
||||
$this->user->defaultUsername->username = 'janedoe';
|
||||
$this->user->defaultUsername->save();
|
||||
|
||||
$token = $this->user->createToken('New');
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
])->json('POST', '/api/auth/logout', []);
|
||||
|
||||
$response->assertStatus(204);
|
||||
$this->assertDatabaseMissing('personal_access_tokens', [
|
||||
'tokenable_id' => $this->user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_can_delete_account_via_api()
|
||||
{
|
||||
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
|
||||
|
||||
$this->user->defaultUsername->username = 'janedoe';
|
||||
$this->user->defaultUsername->save();
|
||||
|
||||
$token = $this->user->createToken('New');
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
])->json('POST', '/api/auth/delete-account', [
|
||||
'password' => 'mypassword',
|
||||
]);
|
||||
|
||||
$response->assertStatus(204);
|
||||
$this->assertDatabaseMissing('usernames', [
|
||||
'username' => 'janedoe',
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_must_enter_correct_password_to_delete_account()
|
||||
{
|
||||
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
|
||||
|
||||
$this->user->defaultUsername->username = 'janedoe';
|
||||
$this->user->defaultUsername->save();
|
||||
|
||||
$token = $this->user->createToken('New');
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
])->json('POST', '/api/auth/delete-account', [
|
||||
'password' => 'incorrect',
|
||||
]);
|
||||
|
||||
$response->assertJsonValidationErrorFor('password');
|
||||
$this->assertDatabaseHas('usernames', [
|
||||
'username' => 'janedoe',
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_must_have_valid_api_key_to_delete_account()
|
||||
{
|
||||
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
|
||||
|
||||
$this->user->defaultUsername->username = 'janedoe';
|
||||
$this->user->defaultUsername->save();
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer invalid-api-key',
|
||||
])->json('POST', '/api/auth/delete-account', [
|
||||
'password' => 'mypassword',
|
||||
]);
|
||||
|
||||
$response->assertUnauthorized();
|
||||
$this->assertDatabaseHas('usernames', [
|
||||
'username' => 'janedoe',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue