Browse Source

Added option to forget aliases

Will Browning 4 years ago
parent
commit
3998811370

+ 1 - 1
README.md

@@ -368,7 +368,7 @@ They would make a Twitter announcement informing all users that they would be ke
 
 ## Is the application tested?
 
-Yes it has over 180 automated PHPUnit tests written.
+Yes it has over 190 automated PHPUnit tests written.
 
 ## How do I host this myself?
 

+ 4 - 0
app/Http/Controllers/Api/ActiveAliasController.php

@@ -12,6 +12,10 @@ class ActiveAliasController extends Controller
     {
         $alias = user()->aliases()->withTrashed()->findOrFail($request->id);
 
+        if ($alias->trashed()) {
+            return response('You need to restore this alias before you can activate it', 422);
+        }
+
         $alias->activate();
 
         return new AliasResource($alias);

+ 28 - 1
app/Http/Controllers/Api/AliasController.php

@@ -39,7 +39,7 @@ class AliasController extends Controller
     public function store(StoreAliasRequest $request)
     {
         if (user()->hasExceededNewAliasLimit()) {
-            return response('', 429);
+            return response('You have reached your hourly limit for creating new aliases', 429);
         }
 
         if (isset($request->validated()['local_part'])) {
@@ -148,4 +148,31 @@ class AliasController extends Controller
 
         return response('', 204);
     }
+
+    public function forget($id)
+    {
+        $alias = user()->aliases()->withTrashed()->findOrFail($id);
+
+        $alias->recipients()->detach();
+
+        if ($alias->hasSharedDomain()) {
+            // Remove all data from the alias and change user_id
+            $alias->update([
+                'user_id' => '00000000-0000-0000-0000-000000000000',
+                'extension' => null,
+                'description' => null,
+                'emails_forwarded' => 0,
+                'emails_blocked' => 0,
+                'emails_replied' => 0,
+                'emails_sent' => 0
+            ]);
+
+            // Soft delete to prevent from being regenerated
+            $alias->delete();
+        } else {
+            $alias->forceDelete();
+        }
+
+        return response('', 204);
+    }
 }

+ 18 - 1
app/Models/Alias.php

@@ -23,6 +23,7 @@ class Alias extends Model
 
     protected $fillable = [
         'id',
+        'user_id',
         'active',
         'description',
         'email',
@@ -30,7 +31,11 @@ class Alias extends Model
         'extension',
         'domain',
         'aliasable_id',
-        'aliasable_type'
+        'aliasable_type',
+        'emails_forwarded',
+        'emails_blocked',
+        'emails_replied',
+        'emails_sent'
     ];
 
     protected $dates = [
@@ -47,6 +52,18 @@ class Alias extends Model
         'active' => 'boolean'
     ];
 
+    public static function boot()
+    {
+        parent::boot();
+
+        // Deactivate the alias when it is deleted
+        Alias::deleting(function ($alias) {
+            if ($alias->active) {
+                $alias->deactivate();
+            }
+        });
+    }
+
     public function setLocalPartAttribute($value)
     {
         $this->attributes['local_part'] = strtolower($value);

+ 16 - 4
app/Models/User.php

@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Foundation\Auth\User as Authenticatable;
 use Illuminate\Notifications\Notifiable;
+use Illuminate\Support\Facades\App;
 use Illuminate\Support\Str;
 use Laravel\Passport\HasApiTokens;
 
@@ -304,10 +305,21 @@ class User extends Authenticatable implements MustVerifyEmail
 
     public function hasExceededNewAliasLimit()
     {
-        return $this
-                ->aliases()
-                ->where('created_at', '>=', now()->subHour())
-                ->count() >= config('anonaddy.new_alias_hourly_limit');
+        if (App::environment('testing')) {
+            return false;
+        }
+
+        return \Illuminate\Support\Facades\Redis::throttle("user:{$this->username}:limit:new-alias")
+            ->allow(config('anonaddy.new_alias_hourly_limit'))
+            ->every(3600)
+            ->then(
+                function () {
+                    return false;
+                },
+                function () {
+                    return true;
+                }
+            );
     }
 
     public function hasReachedAdditionalUsernameLimit()

+ 12 - 17
composer.lock

@@ -1846,16 +1846,16 @@
         },
         {
             "name": "laravel/framework",
-            "version": "v8.41.0",
+            "version": "v8.42.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "05417155d886df8710e55c84e12622b52d83c47c"
+                "reference": "55b886683e0a019bcad0d9d70bb781a3de1a6755"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/05417155d886df8710e55c84e12622b52d83c47c",
-                "reference": "05417155d886df8710e55c84e12622b52d83c47c",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/55b886683e0a019bcad0d9d70bb781a3de1a6755",
+                "reference": "55b886683e0a019bcad0d9d70bb781a3de1a6755",
                 "shasum": ""
             },
             "require": {
@@ -2010,7 +2010,7 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2021-05-11T14:00:02+00:00"
+            "time": "2021-05-18T15:37:44+00:00"
         },
         {
             "name": "laravel/passport",
@@ -9043,16 +9043,16 @@
         },
         {
             "name": "doctrine/annotations",
-            "version": "1.13.0",
+            "version": "1.13.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/annotations.git",
-                "reference": "03cb2123a67d4be806554fe670d0adc298199808"
+                "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/annotations/zipball/03cb2123a67d4be806554fe670d0adc298199808",
-                "reference": "03cb2123a67d4be806554fe670d0adc298199808",
+                "url": "https://api.github.com/repos/doctrine/annotations/zipball/e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f",
+                "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f",
                 "shasum": ""
             },
             "require": {
@@ -9065,7 +9065,7 @@
                 "doctrine/cache": "^1.11 || ^2.0",
                 "doctrine/coding-standard": "^6.0 || ^8.1",
                 "phpstan/phpstan": "^0.12.20",
-                "phpunit/phpunit": "^7.5 || ^9.1.5",
+                "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5",
                 "symfony/cache": "^4.4 || ^5.2"
             },
             "type": "library",
@@ -9109,9 +9109,9 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/annotations/issues",
-                "source": "https://github.com/doctrine/annotations/tree/1.13.0"
+                "source": "https://github.com/doctrine/annotations/tree/1.13.1"
             },
-            "time": "2021-04-29T07:39:39+00:00"
+            "time": "2021-05-16T18:07:53+00:00"
         },
         {
             "name": "doctrine/instantiator",
@@ -9569,11 +9569,6 @@
                 "php-cs-fixer"
             ],
             "type": "application",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.19-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "PhpCsFixer\\": "src/"

+ 2 - 2
config/version.yml

@@ -5,9 +5,9 @@ current:
   major: 0
   minor: 7
   patch: 2
-  prerelease: 8-g8ee7e3b
+  prerelease: 11-g5676033
   buildmetadata: ''
-  commit: 8ee7e3
+  commit: '567603'
   timestamp:
     year: 2020
     month: 10

+ 92 - 90
package-lock.json

@@ -46,16 +46,16 @@
             "integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q=="
         },
         "node_modules/@babel/core": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.2.tgz",
-            "integrity": "sha512-OgC1mON+l4U4B4wiohJlQNUU3H73mpTyYY3j/c8U9dr9UagGGSm+WFpzjy/YLdoyjiG++c1kIDgxCo/mLwQJeQ==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz",
+            "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==",
             "dependencies": {
                 "@babel/code-frame": "^7.12.13",
-                "@babel/generator": "^7.14.2",
+                "@babel/generator": "^7.14.3",
                 "@babel/helper-compilation-targets": "^7.13.16",
                 "@babel/helper-module-transforms": "^7.14.2",
                 "@babel/helpers": "^7.14.0",
-                "@babel/parser": "^7.14.2",
+                "@babel/parser": "^7.14.3",
                 "@babel/template": "^7.12.13",
                 "@babel/traverse": "^7.14.2",
                 "@babel/types": "^7.14.2",
@@ -83,9 +83,9 @@
             }
         },
         "node_modules/@babel/generator": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
-            "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz",
+            "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==",
             "dependencies": {
                 "@babel/types": "^7.14.2",
                 "jsesc": "^2.5.1",
@@ -132,15 +132,15 @@
             }
         },
         "node_modules/@babel/helper-create-class-features-plugin": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.2.tgz",
-            "integrity": "sha512-6YctwVsmlkchxfGUogvVrrhzyD3grFJyluj5JgDlQrwfMLJSt5tdAzFZfPf4H2Xoi5YLcQ6BxfJlaOBHuctyIw==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.3.tgz",
+            "integrity": "sha512-BnEfi5+6J2Lte9LeiL6TxLWdIlEv9Woacc1qXzXBgbikcOzMRM2Oya5XGg/f/ngotv1ej2A/b+3iJH8wbS1+lQ==",
             "dependencies": {
                 "@babel/helper-annotate-as-pure": "^7.12.13",
                 "@babel/helper-function-name": "^7.14.2",
                 "@babel/helper-member-expression-to-functions": "^7.13.12",
                 "@babel/helper-optimise-call-expression": "^7.12.13",
-                "@babel/helper-replace-supers": "^7.13.12",
+                "@babel/helper-replace-supers": "^7.14.3",
                 "@babel/helper-split-export-declaration": "^7.12.13"
             },
             "peerDependencies": {
@@ -148,9 +148,9 @@
             }
         },
         "node_modules/@babel/helper-create-regexp-features-plugin": {
-            "version": "7.12.17",
-            "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz",
-            "integrity": "sha512-p2VGmBu9oefLZ2nQpgnEnG0ZlRPvL8gAGvPUMQwUdaE8k49rOMuZpOwdQoy5qJf6K8jL3bcAMhVUlHAjIgJHUg==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.3.tgz",
+            "integrity": "sha512-JIB2+XJrb7v3zceV2XzDhGIB902CmKGSpSl4q2C6agU9SNLG/2V1RtFRGPG1Ajh9STj3+q6zJMOC+N/pp2P9DA==",
             "dependencies": {
                 "@babel/helper-annotate-as-pure": "^7.12.13",
                 "regexpu-core": "^4.7.1"
@@ -275,14 +275,14 @@
             }
         },
         "node_modules/@babel/helper-replace-supers": {
-            "version": "7.13.12",
-            "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz",
-            "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.3.tgz",
+            "integrity": "sha512-Rlh8qEWZSTfdz+tgNV/N4gz1a0TMNwCUcENhMjHTHKp3LseYH5Jha0NSlyTQWMnjbYcwFt+bqAMqSLHVXkQ6UA==",
             "dependencies": {
                 "@babel/helper-member-expression-to-functions": "^7.13.12",
                 "@babel/helper-optimise-call-expression": "^7.12.13",
-                "@babel/traverse": "^7.13.0",
-                "@babel/types": "^7.13.12"
+                "@babel/traverse": "^7.14.2",
+                "@babel/types": "^7.14.2"
             }
         },
         "node_modules/@babel/helper-simple-access": {
@@ -407,9 +407,9 @@
             }
         },
         "node_modules/@babel/parser": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
-            "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.3.tgz",
+            "integrity": "sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ==",
             "bin": {
                 "parser": "bin/babel-parser.js"
             },
@@ -456,10 +456,11 @@
             }
         },
         "node_modules/@babel/plugin-proposal-class-static-block": {
-            "version": "7.13.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.13.11.tgz",
-            "integrity": "sha512-fJTdFI4bfnMjvxJyNuaf8i9mVcZ0UhetaGEUHaHV9KEnibLugJkZAtXikR8KcYj+NYmI4DZMS8yQAyg+hvfSqg==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.3.tgz",
+            "integrity": "sha512-HEjzp5q+lWSjAgJtSluFDrGGosmwTgKwCXdDQZvhKsRlwv3YdkUEqxNrrjesJd+B9E9zvr1PVPVBvhYZ9msjvQ==",
             "dependencies": {
+                "@babel/helper-create-class-features-plugin": "^7.14.3",
                 "@babel/helper-plugin-utils": "^7.13.0",
                 "@babel/plugin-syntax-class-static-block": "^7.12.13"
             },
@@ -1072,9 +1073,9 @@
             }
         },
         "node_modules/@babel/plugin-transform-runtime": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.2.tgz",
-            "integrity": "sha512-LyA2AiPkaYzI7G5e2YI4NCasTfFe7mZvlupNprDOB7CdNUHb2DQC4uV6oeZ0396gOcicUzUCh0MShL6wiUgk+Q==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.3.tgz",
+            "integrity": "sha512-t960xbi8wpTFE623ef7sd+UpEC5T6EEguQlTBJDEO05+XwnIWVfuqLw/vdLWY6IdFmtZE+65CZAfByT39zRpkg==",
             "dependencies": {
                 "@babel/helper-module-imports": "^7.13.12",
                 "@babel/helper-plugin-utils": "^7.13.0",
@@ -1850,9 +1851,9 @@
             }
         },
         "node_modules/@types/http-proxy": {
-            "version": "1.17.5",
-            "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz",
-            "integrity": "sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q==",
+            "version": "1.17.6",
+            "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.6.tgz",
+            "integrity": "sha512-+qsjqR75S/ib0ig0R9WN+CDoZeOBU6F2XLewgC4KVgdXiNHiKKHFEMRHOrs5PbYE97D5vataw5wPj4KLYfUkuQ==",
             "dependencies": {
                 "@types/node": "*"
             }
@@ -1922,9 +1923,9 @@
             "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA=="
         },
         "node_modules/@types/node": {
-            "version": "15.0.3",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.3.tgz",
-            "integrity": "sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ=="
+            "version": "15.3.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-15.3.0.tgz",
+            "integrity": "sha512-8/bnjSZD86ZfpBsDlCIkNXIvm+h6wi9g7IqL+kmFkQ+Wvu3JrasgLElfiPgoo8V8vVfnEi0QVS12gbl94h9YsQ=="
         },
         "node_modules/@types/parse-glob": {
             "version": "3.0.29",
@@ -4613,9 +4614,9 @@
             "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
         },
         "node_modules/detect-node": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.5.tgz",
-            "integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw=="
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+            "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
         },
         "node_modules/detective": {
             "version": "5.2.0",
@@ -4692,12 +4693,12 @@
             }
         },
         "node_modules/dom-serializer": {
-            "version": "1.3.1",
-            "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz",
-            "integrity": "sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==",
+            "version": "1.3.2",
+            "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
+            "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
             "dependencies": {
                 "domelementtype": "^2.0.1",
-                "domhandler": "^4.0.0",
+                "domhandler": "^4.2.0",
                 "entities": "^2.0.0"
             },
             "funding": {
@@ -4826,9 +4827,9 @@
             "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
         },
         "node_modules/electron-to-chromium": {
-            "version": "1.3.727",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz",
-            "integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg=="
+            "version": "1.3.730",
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.730.tgz",
+            "integrity": "sha512-1Tr3h09wXhmqXnvDyrRe6MFgTeU0ZXy3+rMJWTrOHh/HNesWwBBrKnMxRJWZ86dzs8qQdw2c7ZE1/qeGHygImA=="
         },
         "node_modules/elliptic": {
             "version": "6.5.4",
@@ -14598,16 +14599,16 @@
             "integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q=="
         },
         "@babel/core": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.2.tgz",
-            "integrity": "sha512-OgC1mON+l4U4B4wiohJlQNUU3H73mpTyYY3j/c8U9dr9UagGGSm+WFpzjy/YLdoyjiG++c1kIDgxCo/mLwQJeQ==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz",
+            "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==",
             "requires": {
                 "@babel/code-frame": "^7.12.13",
-                "@babel/generator": "^7.14.2",
+                "@babel/generator": "^7.14.3",
                 "@babel/helper-compilation-targets": "^7.13.16",
                 "@babel/helper-module-transforms": "^7.14.2",
                 "@babel/helpers": "^7.14.0",
-                "@babel/parser": "^7.14.2",
+                "@babel/parser": "^7.14.3",
                 "@babel/template": "^7.12.13",
                 "@babel/traverse": "^7.14.2",
                 "@babel/types": "^7.14.2",
@@ -14627,9 +14628,9 @@
             }
         },
         "@babel/generator": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
-            "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz",
+            "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==",
             "requires": {
                 "@babel/types": "^7.14.2",
                 "jsesc": "^2.5.1",
@@ -14672,22 +14673,22 @@
             }
         },
         "@babel/helper-create-class-features-plugin": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.2.tgz",
-            "integrity": "sha512-6YctwVsmlkchxfGUogvVrrhzyD3grFJyluj5JgDlQrwfMLJSt5tdAzFZfPf4H2Xoi5YLcQ6BxfJlaOBHuctyIw==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.3.tgz",
+            "integrity": "sha512-BnEfi5+6J2Lte9LeiL6TxLWdIlEv9Woacc1qXzXBgbikcOzMRM2Oya5XGg/f/ngotv1ej2A/b+3iJH8wbS1+lQ==",
             "requires": {
                 "@babel/helper-annotate-as-pure": "^7.12.13",
                 "@babel/helper-function-name": "^7.14.2",
                 "@babel/helper-member-expression-to-functions": "^7.13.12",
                 "@babel/helper-optimise-call-expression": "^7.12.13",
-                "@babel/helper-replace-supers": "^7.13.12",
+                "@babel/helper-replace-supers": "^7.14.3",
                 "@babel/helper-split-export-declaration": "^7.12.13"
             }
         },
         "@babel/helper-create-regexp-features-plugin": {
-            "version": "7.12.17",
-            "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz",
-            "integrity": "sha512-p2VGmBu9oefLZ2nQpgnEnG0ZlRPvL8gAGvPUMQwUdaE8k49rOMuZpOwdQoy5qJf6K8jL3bcAMhVUlHAjIgJHUg==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.3.tgz",
+            "integrity": "sha512-JIB2+XJrb7v3zceV2XzDhGIB902CmKGSpSl4q2C6agU9SNLG/2V1RtFRGPG1Ajh9STj3+q6zJMOC+N/pp2P9DA==",
             "requires": {
                 "@babel/helper-annotate-as-pure": "^7.12.13",
                 "regexpu-core": "^4.7.1"
@@ -14805,14 +14806,14 @@
             }
         },
         "@babel/helper-replace-supers": {
-            "version": "7.13.12",
-            "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz",
-            "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.3.tgz",
+            "integrity": "sha512-Rlh8qEWZSTfdz+tgNV/N4gz1a0TMNwCUcENhMjHTHKp3LseYH5Jha0NSlyTQWMnjbYcwFt+bqAMqSLHVXkQ6UA==",
             "requires": {
                 "@babel/helper-member-expression-to-functions": "^7.13.12",
                 "@babel/helper-optimise-call-expression": "^7.12.13",
-                "@babel/traverse": "^7.13.0",
-                "@babel/types": "^7.13.12"
+                "@babel/traverse": "^7.14.2",
+                "@babel/types": "^7.14.2"
             }
         },
         "@babel/helper-simple-access": {
@@ -14927,9 +14928,9 @@
             }
         },
         "@babel/parser": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
-            "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ=="
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.3.tgz",
+            "integrity": "sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ=="
         },
         "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
             "version": "7.13.12",
@@ -14961,10 +14962,11 @@
             }
         },
         "@babel/plugin-proposal-class-static-block": {
-            "version": "7.13.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.13.11.tgz",
-            "integrity": "sha512-fJTdFI4bfnMjvxJyNuaf8i9mVcZ0UhetaGEUHaHV9KEnibLugJkZAtXikR8KcYj+NYmI4DZMS8yQAyg+hvfSqg==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.3.tgz",
+            "integrity": "sha512-HEjzp5q+lWSjAgJtSluFDrGGosmwTgKwCXdDQZvhKsRlwv3YdkUEqxNrrjesJd+B9E9zvr1PVPVBvhYZ9msjvQ==",
             "requires": {
+                "@babel/helper-create-class-features-plugin": "^7.14.3",
                 "@babel/helper-plugin-utils": "^7.13.0",
                 "@babel/plugin-syntax-class-static-block": "^7.12.13"
             }
@@ -15418,9 +15420,9 @@
             }
         },
         "@babel/plugin-transform-runtime": {
-            "version": "7.14.2",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.2.tgz",
-            "integrity": "sha512-LyA2AiPkaYzI7G5e2YI4NCasTfFe7mZvlupNprDOB7CdNUHb2DQC4uV6oeZ0396gOcicUzUCh0MShL6wiUgk+Q==",
+            "version": "7.14.3",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.3.tgz",
+            "integrity": "sha512-t960xbi8wpTFE623ef7sd+UpEC5T6EEguQlTBJDEO05+XwnIWVfuqLw/vdLWY6IdFmtZE+65CZAfByT39zRpkg==",
             "requires": {
                 "@babel/helper-module-imports": "^7.13.12",
                 "@babel/helper-plugin-utils": "^7.13.0",
@@ -16074,9 +16076,9 @@
             }
         },
         "@types/http-proxy": {
-            "version": "1.17.5",
-            "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz",
-            "integrity": "sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q==",
+            "version": "1.17.6",
+            "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.6.tgz",
+            "integrity": "sha512-+qsjqR75S/ib0ig0R9WN+CDoZeOBU6F2XLewgC4KVgdXiNHiKKHFEMRHOrs5PbYE97D5vataw5wPj4KLYfUkuQ==",
             "requires": {
                 "@types/node": "*"
             }
@@ -16146,9 +16148,9 @@
             "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA=="
         },
         "@types/node": {
-            "version": "15.0.3",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.3.tgz",
-            "integrity": "sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ=="
+            "version": "15.3.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-15.3.0.tgz",
+            "integrity": "sha512-8/bnjSZD86ZfpBsDlCIkNXIvm+h6wi9g7IqL+kmFkQ+Wvu3JrasgLElfiPgoo8V8vVfnEi0QVS12gbl94h9YsQ=="
         },
         "@types/parse-glob": {
             "version": "3.0.29",
@@ -18284,9 +18286,9 @@
             "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
         },
         "detect-node": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.5.tgz",
-            "integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw=="
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+            "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
         },
         "detective": {
             "version": "5.2.0",
@@ -18356,12 +18358,12 @@
             }
         },
         "dom-serializer": {
-            "version": "1.3.1",
-            "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz",
-            "integrity": "sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==",
+            "version": "1.3.2",
+            "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
+            "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
             "requires": {
                 "domelementtype": "^2.0.1",
-                "domhandler": "^4.0.0",
+                "domhandler": "^4.2.0",
                 "entities": "^2.0.0"
             },
             "dependencies": {
@@ -18453,9 +18455,9 @@
             "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
         },
         "electron-to-chromium": {
-            "version": "1.3.727",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz",
-            "integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg=="
+            "version": "1.3.730",
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.730.tgz",
+            "integrity": "sha512-1Tr3h09wXhmqXnvDyrRe6MFgTeU0ZXy3+rMJWTrOHh/HNesWwBBrKnMxRJWZ86dzs8qQdw2c7ZE1/qeGHygImA=="
         },
         "elliptic": {
             "version": "6.5.4",

+ 6 - 0
resources/js/components/Icon.vue

@@ -122,6 +122,12 @@
     ></path>
   </svg>
 
+  <svg v-else-if="name === 'rubber'" xmlns="http://www.w3.org/2000/svg" viewBox="-1.5 -2.5 24 24">
+    <path
+      d="M12.728 12.728L8.485 8.485l-5.657 5.657 2.122 2.121a3 3 0 0 0 4.242 0l3.536-3.535zM11.284 17H14a1 1 0 0 1 0 2H3a1 1 0 0 1-.133-1.991l-1.453-1.453a2 2 0 0 1 0-2.828L12.728 1.414a2 2 0 0 1 2.828 0L19.8 5.657a2 2 0 0 1 0 2.828L11.284 17z"
+    ></path>
+  </svg>
+
   <svg
     v-else-if="name === 'blocked'"
     xmlns="http://www.w3.org/2000/svg"

+ 130 - 12
resources/js/pages/Aliases.vue

@@ -483,8 +483,8 @@
         <span v-else-if="props.column.field === 'active'" class="flex items-center">
           <Toggle
             v-model="rows[props.row.originalIndex].active"
-            @on="activateAlias(props.row.id)"
-            @off="deactivateAlias(props.row.id)"
+            @on="activateAlias(rows[props.row.originalIndex])"
+            @off="deactivateAlias(rows[props.row.originalIndex])"
           />
         </span>
         <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
@@ -555,6 +555,29 @@
                 Delete
               </span>
             </div>
+            <div role="none">
+              <span
+                @click="openForgetModal(props.row.id)"
+                class="
+                  group
+                  cursor-pointer
+                  flex
+                  items-center
+                  px-4
+                  py-3
+                  text-sm text-grey-700
+                  hover:bg-grey-100
+                  hover:text-grey-900
+                "
+                role="menuitem"
+              >
+                <icon
+                  name="rubber"
+                  class="block mr-3 w-5 h-5 text-grey-300 fill-current outline-none"
+                />
+                Forget
+              </span>
+            </div>
           </more-options>
         </span>
       </template>
@@ -908,8 +931,8 @@
           Restore alias
         </h2>
         <p class="mt-4 text-grey-700">
-          Are you sure you want to restore this alias? Once restored, this alias will
-          <b>be able to receive emails again</b>.
+          Are you sure you want to restore this alias? Once restored, you will need to set the alias
+          as <b>active before it can receive emails again</b>.
         </p>
         <div class="mt-6">
           <button
@@ -1006,6 +1029,64 @@
       </div>
     </Modal>
 
+    <Modal :open="forgetAliasModalOpen" @close="closeForgetModal">
+      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
+        <h2
+          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
+        >
+          Forget alias
+        </h2>
+        <p class="mt-4 text-grey-700">
+          Are you sure you want to forget this alias? Forgetting an alias will disassociate it from
+          your account.
+        </p>
+        <p class="mt-4 text-grey-700">
+          <b>Note:</b> If this alias uses a shared domain then it can <b>never be restored</b> or
+          used again so make sure you are certain. If it is a standard alias then it can be created
+          again since it will be as if it never existed.
+        </p>
+        <div class="mt-6">
+          <button
+            type="button"
+            @click="forgetAlias(aliasIdToForget)"
+            class="
+              px-4
+              py-3
+              text-white
+              font-semibold
+              bg-red-500
+              hover:bg-red-600
+              border border-transparent
+              rounded
+              focus:outline-none
+            "
+            :class="forgetAliasLoading ? 'cursor-not-allowed' : ''"
+            :disabled="forgetAliasLoading"
+          >
+            Forget alias
+            <loader v-if="forgetAliasLoading" />
+          </button>
+          <button
+            @click="closeForgetModal"
+            class="
+              ml-4
+              px-4
+              py-3
+              text-grey-800
+              font-semibold
+              bg-white
+              hover:bg-grey-50
+              border border-grey-100
+              rounded
+              focus:outline-none
+            "
+          >
+            Cancel
+          </button>
+        </div>
+      </div>
+    </Modal>
+
     <Modal :open="sendFromAliasModalOpen" @close="closeSendFromModal">
       <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
         <h2
@@ -1150,7 +1231,7 @@
               focus:outline-none
             "
           >
-            Cancel
+            Close
           </button>
         </div>
       </div>
@@ -1171,7 +1252,7 @@ import Multiselect from 'vue-multiselect'
 export default {
   props: {
     defaultRecipientEmail: {
-      type: Object,
+      type: String,
       required: true,
     },
     initialAliases: {
@@ -1236,13 +1317,16 @@ export default {
       aliasIdToEdit: '',
       aliasDescriptionToEdit: '',
       aliasIdToDelete: '',
+      aliasIdToForget: '',
       aliasToSendFrom: {},
       sendFromAliasDestination: '',
       sendFromAliasEmailToSendTo: '',
       sendFromAliasCopied: false,
       aliasIdToRestore: '',
       deleteAliasLoading: false,
+      forgetAliasLoading: false,
       deleteAliasModalOpen: false,
+      forgetAliasModalOpen: false,
       sendFromAliasLoading: false,
       sendFromAliasModalOpen: false,
       restoreAliasLoading: false,
@@ -1380,6 +1464,14 @@ export default {
       this.deleteAliasModalOpen = false
       this.aliasIdToDelete = ''
     },
+    openForgetModal(id) {
+      this.forgetAliasModalOpen = true
+      this.aliasIdToForget = id
+    },
+    closeForgetModal() {
+      this.forgetAliasModalOpen = false
+      this.aliasIdToForget = ''
+    },
     openSendFromModal(alias) {
       this.sendFromAliasDestination = ''
       this.sendFromAliasEmailToSendTo = ''
@@ -1427,6 +1519,22 @@ export default {
           this.deleteAliasLoading = false
         })
     },
+    forgetAlias(id) {
+      this.forgetAliasLoading = true
+
+      axios
+        .delete(`/api/v1/aliases/${id}/forget`)
+        .then(response => {
+          this.rows = _.reject(this.rows, alias => alias.id === id)
+          this.forgetAliasModalOpen = false
+          this.forgetAliasLoading = false
+        })
+        .catch(error => {
+          this.error()
+          this.forgetAliasModalOpen = false
+          this.forgetAliasLoading = false
+        })
+    },
     restoreAlias(id) {
       this.restoreAliasLoading = true
 
@@ -1564,12 +1672,12 @@ export default {
           this.error()
         })
     },
-    activateAlias(id) {
+    activateAlias(alias) {
       axios
         .post(
           `/api/v1/active-aliases`,
           JSON.stringify({
-            id: id,
+            id: alias.id,
           }),
           {
             headers: { 'Content-Type': 'application/json' },
@@ -1579,17 +1687,27 @@ export default {
           //
         })
         .catch(error => {
-          this.error()
+          alias.active = false
+          if (error.response !== undefined) {
+            this.error(error.response.data)
+          } else {
+            this.error()
+          }
         })
     },
-    deactivateAlias(id) {
+    deactivateAlias(alias) {
       axios
-        .delete(`/api/v1/active-aliases/${id}`)
+        .delete(`/api/v1/active-aliases/${alias.id}`)
         .then(response => {
           //
         })
         .catch(error => {
-          this.error()
+          alias.active = true
+          if (error.response !== undefined) {
+            this.error(error.response.data)
+          } else {
+            this.error()
+          }
         })
     },
     displaySendFromAddress(alias) {

+ 1 - 0
routes/api.php

@@ -23,6 +23,7 @@ Route::group([
     Route::patch('/aliases/{id}', 'Api\AliasController@update');
     Route::patch('/aliases/{id}/restore', 'Api\AliasController@restore');
     Route::delete('/aliases/{id}', 'Api\AliasController@destroy');
+    Route::delete('/aliases/{id}/forget', 'Api\AliasController@forget');
 
     Route::post('/active-aliases', 'Api\ActiveAliasController@store');
     Route::delete('/active-aliases/{id}', 'Api\ActiveAliasController@destroy');

+ 58 - 1
tests/Feature/Api/AliasesTest.php

@@ -256,13 +256,70 @@ class AliasesTest extends TestCase
     public function user_can_delete_alias()
     {
         $alias = Alias::factory()->create([
-            'user_id' => $this->user->id
+            'user_id' => $this->user->id,
+            'active' => true
         ]);
 
         $response = $this->json('DELETE', '/api/v1/aliases/'.$alias->id);
 
         $response->assertStatus(204);
         $this->assertEmpty($this->user->aliases);
+        $this->assertFalse($alias->refresh()->active);
+    }
+
+    /** @test */
+    public function user_can_forget_alias()
+    {
+        $this->withoutExceptionHandling();
+
+        $alias = Alias::factory()->create([
+            'user_id' => $this->user->id
+        ]);
+
+        $response = $this->json('DELETE', '/api/v1/aliases/'.$alias->id.'/forget');
+
+        $response->assertStatus(204);
+        $this->assertEmpty($this->user->aliases()->withTrashed()->get());
+
+        $this->assertDatabaseMissing('aliases', [
+            'id' => $alias->id
+        ]);
+    }
+
+    /** @test */
+    public function user_can_forget_shared_domain_alias()
+    {
+        $this->withoutExceptionHandling();
+
+        $sharedDomainAlias = Alias::factory()->create([
+            'user_id' => $this->user->id,
+            'domain' => 'anonaddy.me',
+            'local_part' => '9nmrhanm',
+            'email' => '9nmrhanm@anonaddy.me',
+            'extension' => 'ext',
+            'description' => 'Alias',
+            'emails_forwarded' => 10,
+            'emails_blocked' => 1,
+            'emails_replied' => 2,
+            'emails_sent' => 3
+        ]);
+
+        $response = $this->json('DELETE', '/api/v1/aliases/'.$sharedDomainAlias->id.'/forget');
+
+        $response->assertStatus(204);
+        $this->assertEmpty($this->user->aliases()->withTrashed()->get());
+
+        $this->assertDatabaseHas('aliases', [
+            'id' => $sharedDomainAlias->id,
+            'user_id' => '00000000-0000-0000-0000-000000000000',
+            'extension' => null,
+            'description' => null,
+            'emails_forwarded' => 0,
+            'emails_blocked' => 0,
+            'emails_replied' => 0,
+            'emails_sent' => 0,
+            'deleted_at' => now()
+        ]);
     }
 
     /** @test */