Compare commits

...

16 commits

Author SHA1 Message Date
Will Browning
fca7d88894 Updated packages 2024-12-09 16:45:54 +00:00
Will Browning
67c824ce1c Updated packages 2024-11-13 07:49:16 +00:00
Will Browning
abab703ecf Updated packages 2024-10-22 20:49:52 +01:00
Will Browning
e7045cc7f7 Updated API auth routes 2024-09-27 15:38:35 +01:00
Will Browning
c832b1b556 Updated packages 2024-09-04 15:51:29 +01:00
Will Browning
aa0addeacd Added Edge browser extension link 2024-08-28 09:44:50 +01:00
Will Browning
adddee0ba1 Rollback mews/catpcha to v3.3.3 2024-08-27 11:57:41 +01:00
Will Browning
5d0270d880 Fixed failing test 2024-08-23 14:21:50 +01:00
Will Browning
16933763d0 Added "last used at" sort option to aliases 2024-08-23 14:12:18 +01:00
Will Browning
7f2ea49651 Increased custom domain max length 2024-08-02 08:52:16 +01:00
Will Browning
3f789f53ef Updated packages 2024-08-01 17:12:13 +01:00
Will Browning
7916d0c004 Fixed #654 2024-08-01 09:43:28 +01:00
Will Browning
28d685de61 Updated packages 2024-07-30 10:58:10 +01:00
Will Browning
28cf7b475e Added regex options to rule conditions 2024-07-24 12:35:24 +01:00
Will Browning
18e1223b90 Added auto create regex to usernames and domains 2024-07-23 11:41:54 +01:00
Will Browning
0e75d42e3d Added info tooltips 2024-07-03 11:04:03 +01:00
95 changed files with 3363 additions and 2038 deletions

View file

@ -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?

View file

@ -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

View file

@ -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,
],
]);

View file

@ -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) {

View file

@ -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',

View file

@ -220,7 +220,7 @@ class OpenPGPEncrypter
}
if (! $this->gnupg) {
$this->gnupg = new \gnupg();
$this->gnupg = new \gnupg;
}
$this->gnupg->seterrormode(\gnupg::ERROR_EXCEPTION);

View file

@ -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

View file

@ -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');
}
}

View file

@ -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',
]);

View file

@ -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);
}

View file

@ -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'));

View file

@ -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)

View file

@ -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'));

View file

@ -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);
}
}

View file

@ -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),
];
}

View file

@ -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()],
], [

View file

@ -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)

View file

@ -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']),
]);
}
}

View file

@ -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']),
]);
}
}

View 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(),
]);
}
}

View file

@ -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',
],
];
}
}

View file

@ -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',

View 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',
];
}
}

View file

@ -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',

View 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',
];
}
}

View file

@ -29,7 +29,7 @@ class StoreAliasRecipientRequest extends FormRequest
'bail',
'array',
'max:10',
new VerifiedRecipientId(),
new VerifiedRecipientId,
],
];
}

View file

@ -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';

View file

@ -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,
],
];
}

View file

@ -32,8 +32,8 @@ class StoreRecipientRequest extends FormRequest
'string',
'max:254',
'email:rfc',
new UniqueRecipient(),
new NotLocalRecipient(),
new UniqueRecipient,
new NotLocalRecipient,
],
];
}

View file

@ -28,7 +28,7 @@ class StoreReorderRuleRequest extends FormRequest
'ids' => [
'required',
'array',
new ValidRuleId(),
new ValidRuleId,
],
];
}

View file

@ -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',

View file

@ -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,
],
];
}

View 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,
],
];
}
}

View file

@ -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,
],
];
}
}

View file

@ -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,
],
];
}
}

View file

@ -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(),

View file

@ -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,

View file

@ -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(),

View file

@ -158,7 +158,7 @@ class AliasesImport implements ShouldQueue, SkipsEmptyRows, SkipsOnError, SkipsO
'required',
'max:64',
'string',
new ValidAliasLocalPart(),
new ValidAliasLocalPart,
],
'domain' => [
'bail',

View file

@ -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);
}
}
}

View file

@ -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) {

View file

@ -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)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '');
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '')
->replaceMatches('/(This email was sent to).*?(to deactivate this alias)/mis', '');
}
private function removeRealEmailAndHtmlBanner($html)

View file

@ -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)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '');
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/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)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '')
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/')."(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mi", '');
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mis', '')
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/').'(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mis', '');
}
/**

View file

@ -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,

View file

@ -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);

View file

@ -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);
}
/**

View file

@ -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()

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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
View 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.");
}
}
}

View file

@ -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?
}
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ return [
|
*/
'name' => env('APP_NAME', 'Laravel'),
'name' => env('APP_NAME', 'addy.io'),
/*
|--------------------------------------------------------------------------

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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
View file

@ -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": [],

View file

@ -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
}

View file

@ -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',

View file

@ -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'

View file

@ -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."

View file

@ -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 = ''

View file

@ -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">

View file

@ -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

View file

@ -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',

View file

@ -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,
},
{

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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',

View file

@ -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([

View file

@ -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,
]);
}
}

View file

@ -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()
{

View file

@ -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([

View file

@ -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) {

View file

@ -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()
{

View file

@ -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',
]);
}
}