소스 검색

Added catch-all option to additional usernames

Will 4 년 전
부모
커밋
6a07f84fc1

+ 21 - 5
SELF-HOSTING.md

@@ -547,27 +547,43 @@ exit
 
 Create a new file `/etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf` and enter the following inside:
 
-```
+```sql
 user = anonaddy
 password = your-database-password
 hosts = 127.0.0.1
 dbname = anonaddy_database
-query = SELECT (SELECT 1 FROM users WHERE CONCAT(username, '.example.com') = '%s') AS users, (SELECT 1 FROM additional_usernames WHERE CONCAT(additional_usernames.username, '.example.com') = '%s') AS usernames, (SELECT 1 FROM domains WHERE domains.domain = '%s' AND domains.domain_verified_at IS NOT NULL) AS domains LIMIT 1;
+query = SELECT (SELECT 1 FROM users WHERE '%s' IN (CONCAT(username, '.example.com'))) AS users, (SELECT 1 FROM additional_usernames WHERE '%s' IN (CONCAT(additional_usernames.username, '.example.com'))) AS usernames, (SELECT 1 FROM domains WHERE domains.domain = '%s' AND domains.domain_verified_at IS NOT NULL) AS domains LIMIT 1;
+```
+
+If you need to add multiple domains then just update the above query to:
+
+```sql
+query = SELECT (SELECT 1 FROM users WHERE '%s' IN (CONCAT(username, '.example.com'),CONCAT(username, '.example2.com'))) AS users, (SELECT 1 FROM additional_usernames WHERE '%s' IN (CONCAT(additional_usernames.username, '.example.com'),CONCAT(additional_usernames.username, '.example2.com'))) AS usernames, (SELECT 1 FROM domains WHERE domains.domain = '%s' AND domains.domain_verified_at IS NOT NULL) AS domains LIMIT 1;
 ```
 
 This file is responsible for determining whether the server should accept email for a certain domain/subdomain. If no results are found from the query then the email will not be accepted.
 
 Next create another new file `/etc/postfix/mysql-recipient-access-domains-and-additional-usernames.cf` and enter the following inside:
 
-```
+```sql
 user = anonaddy
 password = your-database-password
 hosts = 127.0.0.1
 dbname = anonaddy_database
-query = SELECT (SELECT 'DISCARD' FROM additional_usernames WHERE (CONCAT(username, '.example.com') = SUBSTRING_INDEX('%s','@',-1)) AND active = 0) AS usernames, (SELECT CASE WHEN NOT EXISTS(SELECT NULL FROM aliases WHERE email='%s') AND catch_all = 0 THEN 'REJECT' WHEN active=0 THEN 'DISCARD' ELSE NULL END FROM domains WHERE domain = SUBSTRING_INDEX('%s','@',-1)) AS domains LIMIT 1;
+query = SELECT (SELECT CASE WHEN NOT EXISTS(SELECT NULL FROM aliases WHERE email = '%s') AND additional_usernames.catch_all = 0 OR domains.catch_all = 0 THEN "REJECT" WHEN additional_usernames.active = 0 OR domains.active = 0 THEN "DISCARD" ELSE NULL END FROM additional_usernames, domains WHERE SUBSTRING_INDEX('%s', '@',-1) IN (CONCAT(additional_usernames.username, '.example.com')) OR domains.domain = SUBSTRING_INDEX('%s', '@',-1) LIMIT 1) AS result LIMIT 1;
 ```
 
-This file is responsible for checking whether the alias is for an additional username/custom domain and if so then is that additional username/custom domain set as active. If it is not set as active then the email is discarded.
+If you need to add multiple domains then just update the IN section to:
+
+```sql
+IN (CONCAT(additional_usernames.username, '.example.com'),CONCAT(additional_usernames.username, '.example2.com'))
+```
+
+etc.
+
+This file is responsible for checking whether the alias is for an additional username/custom domain and if so then is that additional username/custom domain set as active. If it is not set as active then the email is discarded. It also checks if the additional usename/custom domain has catch-all enabled and if not it checks if that alias already exists. If it does not already exist then the email is rejected.
+
+The reason these SQL queries are not all nicely formatted is because they have to be on one line.
 
 Now we need to create a stored procedure that can be called.
 

+ 2 - 2
app/Console/Commands/ReceiveEmail.php

@@ -177,7 +177,7 @@ class ReceiveEmail extends Command
 
     protected function handleSendFrom($user, $recipient, $aliasable)
     {
-        $alias = $user->aliases()->firstOrNew([
+        $alias = $user->aliases()->withTrashed()->firstOrNew([
             'email' => $recipient['local_part'] . '@' . $recipient['domain'],
             'local_part' => $recipient['local_part'],
             'domain' => $recipient['domain'],
@@ -204,7 +204,7 @@ class ReceiveEmail extends Command
 
     protected function handleForward($user, $recipient, $aliasable)
     {
-        $alias = $user->aliases()->firstOrNew([
+        $alias = $user->aliases()->withTrashed()->firstOrNew([
             'email' => $recipient['local_part'] . '@' . $recipient['domain'],
             'local_part' => $recipient['local_part'],
             'domain' => $recipient['domain'],

+ 28 - 0
app/Http/Controllers/Api/CatchAllAdditionalUsernameController.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\Http\Resources\AdditionalUsernameResource;
+use Illuminate\Http\Request;
+
+class CatchAllAdditionalUsernameController extends Controller
+{
+    public function store(Request $request)
+    {
+        $username = user()->additionalUsernames()->findOrFail($request->id);
+
+        $username->enableCatchAll();
+
+        return new AdditionalUsernameResource($username);
+    }
+
+    public function destroy($id)
+    {
+        $username = user()->additionalUsernames()->findOrFail($id);
+
+        $username->disableCatchAll();
+
+        return response('', 204);
+    }
+}

+ 1 - 0
app/Http/Resources/AdditionalUsernameResource.php

@@ -16,6 +16,7 @@ class AdditionalUsernameResource extends JsonResource
             'aliases' => AliasResource::collection($this->whenLoaded('aliases')),
             'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
             'active' => $this->active,
+            'catch_all' => $this->catch_all,
             'created_at' => $this->created_at->toDateTimeString(),
             'updated_at' => $this->updated_at->toDateTimeString(),
         ];

+ 19 - 1
app/Models/AdditionalUsername.php

@@ -22,7 +22,8 @@ class AdditionalUsername extends Model
     protected $fillable = [
         'username',
         'description',
-        'active'
+        'active',
+        'catch_all',
     ];
 
     protected $dates = [
@@ -34,6 +35,7 @@ class AdditionalUsername extends Model
         'id' => 'string',
         'user_id' => 'string',
         'active' => 'boolean',
+        'catch_all' => 'boolean',
         'default_recipient_id' => 'string'
     ];
 
@@ -102,4 +104,20 @@ class AdditionalUsername extends Model
     {
         $this->update(['active' => true]);
     }
+
+    /**
+     * Disable catch-all for the username.
+     */
+    public function disableCatchAll()
+    {
+        $this->update(['catch_all' => false]);
+    }
+
+    /**
+     * Enable catch-all for the username.
+     */
+    public function enableCatchAll()
+    {
+        $this->update(['catch_all' => true]);
+    }
 }

+ 32 - 0
database/migrations/2020_10_09_115344_add_catch_all_to_additional_usernames_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddCatchAllToAdditionalUsernamesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('additional_usernames', function (Blueprint $table) {
+            $table->boolean('catch_all')->after('active')->default(true);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('additional_usernames', function (Blueprint $table) {
+            $table->dropColumn('catch_all');
+        });
+    }
+}

+ 6 - 6
package-lock.json

@@ -9425,9 +9425,9 @@
             "dev": true
         },
         "tailwindcss": {
-            "version": "1.8.11",
-            "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.8.11.tgz",
-            "integrity": "sha512-k7Mam6s+rmr79z0eil56tdGH7cvTlzWfijlO5qDLlpybDqPwzYRGQ/1CksE/H192oeAem1bFy9IDJL97q7lSag==",
+            "version": "1.8.12",
+            "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.8.12.tgz",
+            "integrity": "sha512-VChYp+4SduP8hHFAmf75P5Yf0qNQi3oSSnpEMKkC6kWW/9K+SizRgbmllqLJLnTZq+eM3TDwvn1jWXvvg+dfDA==",
             "requires": {
                 "@fullhuman/postcss-purgecss": "^2.1.2",
                 "autoprefixer": "^9.4.5",
@@ -9473,9 +9473,9 @@
                     }
                 },
                 "caniuse-lite": {
-                    "version": "1.0.30001144",
-                    "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001144.tgz",
-                    "integrity": "sha512-4GQTEWNMnVZVOFG3BK0xvGeaDAtiPAbG2N8yuMXuXzx/c2Vd4XoMPO8+E918zeXn5IF0FRVtGShBfkfQea2wHQ=="
+                    "version": "1.0.30001146",
+                    "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz",
+                    "integrity": "sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug=="
                 },
                 "chalk": {
                     "version": "4.1.0",

+ 1 - 1
package.json

@@ -21,7 +21,7 @@
         "postcss-import": "^11.1.0",
         "postcss-nesting": "^5.0.0",
         "resolve-url-loader": "^2.3.2",
-        "tailwindcss": "^1.8.11",
+        "tailwindcss": "^1.8.12",
         "tippy.js": "^4.3.5",
         "v-clipboard": "^2.2.3",
         "vue": "^2.6.12",

+ 49 - 0
resources/js/pages/Usernames.vue

@@ -142,6 +142,13 @@
             @off="deactivateUsername(props.row.id)"
           />
         </span>
+        <span v-else-if="props.column.field === 'catch_all'" class="flex items-center">
+          <Toggle
+            v-model="rows[props.row.originalIndex].catch_all"
+            @on="enableCatchAll(props.row.id)"
+            @off="disableCatchAll(props.row.id)"
+          />
+        </span>
         <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
           <icon
             name="trash"
@@ -378,6 +385,12 @@ export default {
           type: 'boolean',
           globalSearchDisabled: true,
         },
+        {
+          label: 'Catch-All',
+          field: 'catch_all',
+          type: 'boolean',
+          globalSearchDisabled: true,
+        },
         {
           label: '',
           field: 'actions',
@@ -552,6 +565,42 @@ export default {
           this.error()
         })
     },
+    enableCatchAll(id) {
+      axios
+        .post(
+          `/api/v1/catch-all-usernames`,
+          JSON.stringify({
+            id: id,
+          }),
+          {
+            headers: { 'Content-Type': 'application/json' },
+          }
+        )
+        .then(response => {
+          //
+        })
+        .catch(error => {
+          if (error.response !== undefined) {
+            this.error(error.response.data)
+          } else {
+            this.error()
+          }
+        })
+    },
+    disableCatchAll(id) {
+      axios
+        .delete(`/api/v1/catch-all-usernames/${id}`)
+        .then(response => {
+          //
+        })
+        .catch(error => {
+          if (error.response !== undefined) {
+            this.error(error.response.data)
+          } else {
+            this.error()
+          }
+        })
+    },
     deleteUsername(id) {
       this.deleteUsernameLoading = true
 

+ 3 - 0
routes/api.php

@@ -63,6 +63,9 @@ Route::group([
     Route::post('/active-usernames', 'Api\ActiveAdditionalUsernameController@store');
     Route::delete('/active-usernames/{id}', 'Api\ActiveAdditionalUsernameController@destroy');
 
+    Route::post('/catch-all-usernames', 'Api\CatchAllAdditionalUsernameController@store');
+    Route::delete('/catch-all-usernames/{id}', 'Api\CatchAllAdditionalUsernameController@destroy');
+
     Route::get('/rules', 'Api\RuleController@index');
     Route::get('/rules/{id}', 'Api\RuleController@show');
     Route::post('/rules', 'Api\RuleController@store');

+ 1 - 0
tailwind.config.js

@@ -1,6 +1,7 @@
 module.exports = {
   future: {
     removeDeprecatedGapUtilities: true,
+    purgeLayersByDefault: true,
   },
   theme: {
     colors: {

+ 30 - 0
tests/Feature/Api/AdditionalUsernamesTest.php

@@ -189,6 +189,36 @@ class AdditionalUsernamesTest extends TestCase
         $this->assertFalse($this->user->additionalUsernames[0]->active);
     }
 
+    /** @test */
+    public function user_can_enable_catch_all_for_additional_username()
+    {
+        $username = AdditionalUsername::factory()->create([
+            'user_id' => $this->user->id,
+            'catch_all' => false
+        ]);
+
+        $response = $this->json('POST', '/api/v1/catch-all-usernames/', [
+            'id' => $username->id
+        ]);
+
+        $response->assertStatus(200);
+        $this->assertTrue($response->getData()->data->catch_all);
+    }
+
+    /** @test */
+    public function user_can_disable_catch_all_for_additional_username()
+    {
+        $username = AdditionalUsername::factory()->create([
+            'user_id' => $this->user->id,
+            'catch_all' => true
+        ]);
+
+        $response = $this->json('DELETE', '/api/v1/catch-all-usernames/'.$username->id);
+
+        $response->assertStatus(204);
+        $this->assertFalse($this->user->additionalUsernames[0]->catch_all);
+    }
+
     /** @test */
     public function user_can_update_additional_usernames_description()
     {