Skip to content

Commit

Permalink
Added catch-all option to additional usernames
Browse files Browse the repository at this point in the history
  • Loading branch information
Will committed Oct 9, 2020
1 parent 0fe31fe commit 6a07f84
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 15 deletions.
26 changes: 21 additions & 5 deletions SELF-HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions app/Console/Commands/ReceiveEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ protected function handleReply($user, $recipient)

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'],
Expand All @@ -204,7 +204,7 @@ protected function handleSendFrom($user, $recipient, $aliasable)

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'],
Expand Down
28 changes: 28 additions & 0 deletions app/Http/Controllers/Api/CatchAllAdditionalUsernameController.php
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions app/Http/Resources/AdditionalUsernameResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public function toArray($request)
'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(),
];
Expand Down
20 changes: 19 additions & 1 deletion app/Models/AdditionalUsername.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class AdditionalUsername extends Model
protected $fillable = [
'username',
'description',
'active'
'active',
'catch_all',
];

protected $dates = [
Expand All @@ -34,6 +35,7 @@ class AdditionalUsername extends Model
'id' => 'string',
'user_id' => 'string',
'active' => 'boolean',
'catch_all' => 'boolean',
'default_recipient_id' => 'string'
];

Expand Down Expand Up @@ -102,4 +104,20 @@ public function activate()
{
$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]);
}
}
Original file line number Diff line number Diff line change
@@ -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');
});
}
}
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
49 changes: 49 additions & 0 deletions resources/js/pages/Usernames.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -378,6 +385,12 @@ export default {
type: 'boolean',
globalSearchDisabled: true,
},
{
label: 'Catch-All',
field: 'catch_all',
type: 'boolean',
globalSearchDisabled: true,
},
{
label: '',
field: 'actions',
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
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');
Expand Down
1 change: 1 addition & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
future: {
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
},
theme: {
colors: {
Expand Down
30 changes: 30 additions & 0 deletions tests/Feature/Api/AdditionalUsernamesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,36 @@ public function user_can_deactivate_additional_username()
$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()
{
Expand Down

0 comments on commit 6a07f84

Please sign in to comment.