Browse Source

Fix for #47 and other minor improvements

Will 4 years ago
parent
commit
8ecc24657b

+ 37 - 41
app/Console/Commands/ReceiveEmail.php

@@ -74,45 +74,51 @@ class ReceiveEmail extends Command
             $this->size = $this->option('size') / ($recipientCount ? $recipientCount : 1);
 
             foreach ($recipients as $recipient) {
-                $parentDomain = collect(config('anonaddy.all_domains'))
+
+                // First determine if the alias already exists in the database
+                if ($alias = Alias::where('email', $recipient['local_part'] . '@' . $recipient['domain'])->first()) {
+                    $user = $alias->user;
+
+                    if ($alias->aliasable_id) {
+                        $aliasable = $alias->aliasable;
+                    }
+                } else {
+                    // Does not exist, must be a standard, additional username or custom domain alias
+                    $parentDomain = collect(config('anonaddy.all_domains'))
                     ->filter(function ($name) use ($recipient) {
                         return Str::endsWith($recipient['domain'], $name);
                     })
                     ->first();
 
-                if ($parentDomain) {
-                    $subdomain = substr($recipient['domain'], 0, strrpos($recipient['domain'], '.'.$parentDomain));
+                    if (!empty($parentDomain)) {
+                        // It is standard or additional username alias
+                        $subdomain = substr($recipient['domain'], 0, strrpos($recipient['domain'], '.' . $parentDomain)); // e.g. johndoe
 
-                    if ($subdomain === 'unsubscribe') {
-                        $this->handleUnsubscribe($recipient);
-                        continue;
-                    }
+                        if ($subdomain === 'unsubscribe') {
+                            $this->handleUnsubscribe($recipient);
+                            continue;
+                        }
 
-                    // Check if this is an additional username.
-                    if ($additionalUsername = AdditionalUsername::where('username', $subdomain)->first()) {
-                        $user = $additionalUsername->user;
-                        $aliasable = $additionalUsername;
-                    } else {
-                        $user = User::where('username', $subdomain)->first();
-                    }
-                }
+                        // Check if this is an additional username or standard alias
+                        if (!empty($subdomain)) {
+                            $user = User::where('username', $subdomain)->first();
 
-                if (!isset($user)) {
-                    // Check if this is a custom domain.
-                    if ($customDomain = Domain::where('domain', $recipient['domain'])->first()) {
-                        $user = $customDomain->user;
-                        $aliasable = $customDomain;
+                            if (!isset($user)) {
+                                $additionalUsername = AdditionalUsername::where('username', $subdomain)->first();
+                                $user = $additionalUsername->user;
+                                $aliasable = $additionalUsername;
+                            }
+                        }
+                    } else {
+                        // It is a custom domain
+                        if ($customDomain = Domain::where('domain', $recipient['domain'])->first()) {
+                            $user = $customDomain->user;
+                            $aliasable = $customDomain;
+                        }
                     }
 
-                    // check if this is a uuid generated alias
-                    if ($alias = Alias::find($recipient['local_part'])) {
-                        $user = $alias->user;
-                    } elseif ($recipient['domain'] === $parentDomain) {
-                        if ($alias = Alias::where('email', $recipient['local_part'] . '@' . $recipient['domain'])->first()) {
-                            $user = $alias->user;
-                        } elseif (!empty(config('anonaddy.admin_username'))) {
-                            $user = User::where('username', config('anonaddy.admin_username'))->first();
-                        }
+                    if (!isset($user) && !empty(config('anonaddy.admin_username'))) {
+                        $user = User::where('username', config('anonaddy.admin_username'))->first();
                     }
                 }
 
@@ -175,15 +181,10 @@ class ReceiveEmail extends Command
             'email' => $recipient['local_part'] . '@' . $recipient['domain'],
             'local_part' => $recipient['local_part'],
             'domain' => $recipient['domain'],
+            'aliasable_id' => $aliasable->id ?? null,
             'aliasable_type' => $aliasable ? 'App\\Models\\' . class_basename($aliasable) : null
         ]);
 
-        $aliasableId = $aliasable->id ?? null;
-
-        if ($alias->aliasable_id !== $aliasableId) {
-            $alias->aliasable_id = $aliasableId;
-        }
-
         // This is a new alias but at a shared domain or the sender is not a verified recipient.
         if (!isset($alias->id) && in_array($recipient['domain'], config('anonaddy.all_domains'))) {
             exit(0);
@@ -207,15 +208,10 @@ class ReceiveEmail extends Command
             'email' => $recipient['local_part'] . '@' . $recipient['domain'],
             'local_part' => $recipient['local_part'],
             'domain' => $recipient['domain'],
+            'aliasable_id' => $aliasable->id ?? null,
             'aliasable_type' => $aliasable ? 'App\\Models\\' . class_basename($aliasable) : null
         ]);
 
-        $aliasableId = $aliasable->id ?? null;
-
-        if ($alias->aliasable_id !== $aliasableId) {
-            $alias->aliasable_id = $aliasableId;
-        }
-
         if (!isset($alias->id)) {
             // This is a new alias.
             if ($user->hasExceededNewAliasLimit()) {

+ 6 - 1
app/Http/Controllers/ShowAdditionalUsernameController.php

@@ -7,7 +7,12 @@ class ShowAdditionalUsernameController extends Controller
     public function index()
     {
         return view('usernames.index', [
-            'usernames' => user()->additionalUsernames()->with(['aliases', 'defaultRecipient'])->latest()->get()
+            'usernames' => user()
+                ->additionalUsernames()
+                ->with('defaultRecipient:id,email')
+                ->withCount('aliases')
+                ->latest()
+                ->get()
         ]);
     }
 }

+ 20 - 8
app/Http/Controllers/ShowAliasController.php

@@ -6,18 +6,30 @@ class ShowAliasController extends Controller
 {
     public function index()
     {
+        $totals = user()
+            ->aliases()
+            ->withTrashed()
+            ->toBase()
+            ->selectRaw("sum(emails_forwarded) as forwarded")
+            ->selectRaw("sum(emails_blocked) as blocked")
+            ->selectRaw("sum(emails_replied) as replies")
+            ->first();
+
         return view('aliases.index', [
+            'user' => user(),
             'defaultRecipient' => user()->defaultRecipient,
-            'aliases' => user()->aliases()->with(['recipients', 'aliasable.defaultRecipient'])->latest()->get(),
-            'recipients' => user()->verifiedRecipients,
-            'totalForwarded' => user()->totalEmailsForwarded(),
-            'totalBlocked' => user()->totalEmailsBlocked(),
-            'totalReplies' => user()->totalEmailsReplied(),
+            'aliases' => user()
+                ->aliases()
+                ->with([
+                    'recipients:recipient_id,email',
+                    'aliasable.defaultRecipient:id,email'
+                ])
+                ->latest()
+                ->get(),
+            'recipients' => user()->verifiedRecipients()->select(['id', 'email'])->get(),
+            'totals' => $totals,
             'domain' => user()->username.'.'.config('anonaddy.domain'),
-            'bandwidthMb' => user()->bandwidth_mb,
             'domainOptions' => user()->domainOptions(),
-            'defaultAliasDomain' => user()->default_alias_domain,
-            'defaultAliasFormat' => user()->default_alias_format
         ]);
     }
 }

+ 6 - 1
app/Http/Controllers/ShowDomainController.php

@@ -7,7 +7,12 @@ class ShowDomainController extends Controller
     public function index()
     {
         return view('domains.index', [
-            'domains' => user()->domains()->with(['aliases', 'defaultRecipient'])->latest()->get()
+            'domains' => user()
+                ->domains()
+                ->with('defaultRecipient:id,email')
+                ->withCount('aliases')
+                ->latest()
+                ->get()
         ]);
     }
 }

+ 23 - 2
app/Http/Controllers/ShowRecipientController.php

@@ -6,7 +6,27 @@ class ShowRecipientController extends Controller
 {
     public function index()
     {
-        $recipients = user()->recipients()->with('aliases')->latest()->get();
+        $recipients = user()->recipients()->with([
+            'aliases:alias_id,aliasable_id,email',
+            'domainsUsingAsDefault.aliases:id,aliasable_id,email',
+            'AdditionalUsernamesUsingAsDefault.aliases:id,aliasable_id,email'
+        ])->latest()->get();
+
+        $recipients->each(function ($recipient) {
+            if ($recipient->domainsUsingAsDefault) {
+                $domainAliases = $recipient->domainsUsingAsDefault->flatMap(function ($domain) {
+                    return $domain->aliases;
+                });
+                $recipient->setRelation('aliases', $recipient->aliases->concat($domainAliases)->unique('email'));
+            }
+
+            if ($recipient->AdditionalUsernamesUsingAsDefault) {
+                $AdditionalUsernameAliases = $recipient->AdditionalUsernamesUsingAsDefault->flatMap(function ($domain) {
+                    return $domain->aliases;
+                });
+                $recipient->setRelation('aliases', $recipient->aliases->concat($AdditionalUsernameAliases)->unique('email'));
+            }
+        });
 
         $count = $recipients->count();
 
@@ -16,7 +36,8 @@ class ShowRecipientController extends Controller
 
         return view('recipients.index', [
             'recipients' => $recipients,
-            'aliasesUsingDefault' => user()->aliasesUsingDefault
+            'aliasesUsingDefault' => user()->aliasesUsingDefault()->take(5)->get(),
+            'aliasesUsingDefaultCount' => user()->aliasesUsingDefault()->count(),
         ]);
     }
 }

+ 4 - 1
app/Http/Controllers/ShowRuleController.php

@@ -7,7 +7,10 @@ class ShowRuleController extends Controller
     public function index()
     {
         return view('rules.index', [
-            'rules' => user()->rules()->orderBy('order')->get()
+            'rules' => user()
+                ->rules()
+                ->orderBy('order')
+                ->get()
         ]);
     }
 }

+ 3 - 1
app/Http/Requests/StoreDomainRequest.php

@@ -3,6 +3,7 @@
 namespace App\Http\Requests;
 
 use App\Rules\NotLocalDomain;
+use App\Rules\NotUsedAsRecipientDomain;
 use App\Rules\ValidDomain;
 use Illuminate\Foundation\Http\FormRequest;
 
@@ -32,7 +33,8 @@ class StoreDomainRequest extends FormRequest
                 'max:50',
                 'unique:domains',
                 new ValidDomain,
-                new NotLocalDomain
+                new NotLocalDomain,
+                new NotUsedAsRecipientDomain
             ]
         ];
     }

+ 16 - 0
app/Models/Recipient.php

@@ -73,6 +73,22 @@ class Recipient extends Model
         return $this->belongsToMany(Alias::class, 'alias_recipients')->using(AliasRecipient::class);
     }
 
+    /**
+     * Get all of the user's custom domains.
+     */
+    public function domainsUsingAsDefault()
+    {
+        return $this->hasMany(Domain::class, 'default_recipient_id', 'id');
+    }
+
+    /**
+     * Get all of the user's custom domains.
+     */
+    public function additionalUsernamesUsingAsDefault()
+    {
+        return $this->hasMany(AdditionalUsername::class, 'default_recipient_id', 'id');
+    }
+
     /**
      * Determine if the recipient has a verified email address.
      *

+ 10 - 1
app/Models/User.php

@@ -5,6 +5,7 @@ namespace App\Models;
 use App\Traits\HasEncryptedAttributes;
 use App\Traits\HasUuid;
 use Illuminate\Contracts\Auth\MustVerifyEmail;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Foundation\Auth\User as Authenticatable;
 use Illuminate\Notifications\Notifiable;
@@ -215,7 +216,15 @@ class User extends Authenticatable implements MustVerifyEmail
      */
     public function aliasesUsingDefault()
     {
-        return $this->aliases()->whereDoesntHave('recipients');
+        return $this->aliases()->whereDoesntHave('recipients')->where(function (Builder $q) {
+            return $q->whereDoesntHaveMorph(
+                'aliasable',
+                ['App\Models\Domain', 'App\Models\AdditionalUsername'],
+                function (Builder $query) {
+                    $query->whereNotNull('default_recipient_id');
+                }
+            )->orWhereNull('aliasable_id');
+        });
     }
 
     public function hasVerifiedDefaultRecipient()

+ 6 - 1
app/Rules/NotLocalRecipient.php

@@ -2,6 +2,7 @@
 
 namespace App\Rules;
 
+use App\Models\Domain;
 use Illuminate\Contracts\Validation\Rule;
 use Illuminate\Support\Str;
 
@@ -28,7 +29,11 @@ class NotLocalRecipient implements Rule
     {
         $emailDomain = Str::afterLast($value, '@');
 
+        // Make sure the recipient domain is not added as a verified custom domain
+        $customDomains = Domain::whereNotNull('domain_verified_at')->pluck('domain');
+
         $count = collect(config('anonaddy.all_domains'))
+            ->concat($customDomains)
             ->filter(function ($domain) use ($emailDomain) {
                 return Str::endsWith(strtolower($emailDomain), $domain);
             })
@@ -44,6 +49,6 @@ class NotLocalRecipient implements Rule
      */
     public function message()
     {
-        return 'The recipient cannot be a local one or alias.';
+        return 'The recipient cannot use a local domain or be an alias.';
     }
 }

+ 49 - 0
app/Rules/NotUsedAsRecipientDomain.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Rules;
+
+use App\Models\Recipient;
+use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Support\Str;
+
+class NotUsedAsRecipientDomain implements Rule
+{
+    /**
+     * Create a new rule instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Determine if the validation rule passes.
+     *
+     * @param  string  $attribute
+     * @param  mixed  $value
+     * @return bool
+     */
+    public function passes($attribute, $value)
+    {
+        return ! Recipient::whereNotNull('email_verified_at')
+            ->get()
+            ->pluck('email')
+            ->map(function ($recipientEmail) {
+                return Str::afterLast($recipientEmail, '@');
+            })
+            ->unique()
+            ->contains($value);
+    }
+
+    /**
+     * Get the validation error message.
+     *
+     * @return string
+     */
+    public function message()
+    {
+        return 'The domain must not already be used by a verified recipient.';
+    }
+}

+ 1 - 1
resources/js/pages/Domains.vue

@@ -134,7 +134,7 @@
           </div>
         </span>
         <span v-else-if="props.column.field === 'aliases_count'">
-          {{ props.row.aliases.length }}
+          {{ props.row.aliases_count }}
         </span>
         <span v-else-if="props.column.field === 'active'" class="flex items-center">
           <Toggle

+ 19 - 4
resources/js/pages/Recipients.vue

@@ -102,9 +102,15 @@
           <span
             v-if="props.row.aliases.length"
             class="tooltip outline-none"
-            :data-tippy-content="aliasesTooltip(props.row.aliases)"
+            :data-tippy-content="aliasesTooltip(props.row.aliases, isDefault(props.row.id))"
             >{{ props.row.aliases[0].email | truncate(40) }}
-            <span v-if="props.row.aliases.length > 1" class="block text-grey-500 text-sm">
+            <span
+              v-if="isDefault(props.row.id) && aliasesUsingDefaultCount > 1"
+              class="block text-grey-500 text-sm"
+            >
+              + {{ aliasesUsingDefaultCount - 1 }}</span
+            >
+            <span v-else-if="props.row.aliases.length > 1" class="block text-grey-500 text-sm">
               + {{ props.row.aliases.length - 1 }}</span
             >
           </span>
@@ -338,6 +344,10 @@ export default {
       type: Array,
       required: true,
     },
+    aliasesUsingDefaultCount: {
+      type: Number,
+      required: true,
+    },
     domain: {
       type: String,
       required: true,
@@ -431,8 +441,13 @@ export default {
     debounceToolips: _.debounce(function() {
       this.addTooltips()
     }, 50),
-    aliasesTooltip(aliases) {
-      return _.reduce(aliases, (list, alias) => list + `${alias.email}<br>`, '')
+    aliasesTooltip(aliases, isDefault) {
+      let ellipses =
+        aliases.length > 5 || (isDefault && this.aliasesUsingDefaultCount > 5) ? '...' : ''
+
+      return (
+        _.reduce(_.take(aliases, 5), (list, alias) => list + `${alias.email}<br>`, '') + ellipses
+      )
     },
     isDefault(id) {
       return this.user.default_recipient_id === id

+ 1 - 1
resources/js/pages/Usernames.vue

@@ -133,7 +133,7 @@
           </div>
         </span>
         <span v-else-if="props.column.field === 'aliases_count'">
-          {{ props.row.aliases.length }}
+          {{ props.row.aliases_count }}
         </span>
         <span v-else-if="props.column.field === 'active'" class="flex items-center">
           <Toggle

+ 1 - 1
resources/views/aliases/index.blade.php

@@ -4,6 +4,6 @@
     <div class="container py-8">
         @include('shared.status')
 
-        <aliases :default-recipient="{{json_encode($defaultRecipient)}}" :initial-aliases="{{json_encode($aliases)}}" :recipient-options="{{json_encode($recipients)}}" :total-forwarded="{{$totalForwarded}}" :total-blocked="{{$totalBlocked}}" :total-replies="{{$totalReplies}}" domain="{{config('anonaddy.domain')}}" subdomain="{{$domain}}" :bandwidth-mb="{{$bandwidthMb}}" :month="{{json_encode(now()->format('M'))}}" :domain-options="{{$domainOptions}}" default-alias-domain="{{$defaultAliasDomain}}" default-alias-format="{{$defaultAliasFormat}}" />
+        <aliases :default-recipient="{{json_encode($defaultRecipient)}}" :initial-aliases="{{json_encode($aliases)}}" :recipient-options="{{json_encode($recipients)}}" :total-forwarded="{{$totals->forwarded}}" :total-blocked="{{$totals->blocked}}" :total-replies="{{$totals->replies}}" domain="{{config('anonaddy.domain')}}" subdomain="{{$domain}}" :bandwidth-mb="{{$user->bandwidth_mb}}" :month="{{json_encode(now()->format('M'))}}" :domain-options="{{$domainOptions}}" default-alias-domain="{{$user->default_alias_domain}}" default-alias-format="{{$user->default_alias_format}}" />
     </div>
 @endsection

+ 1 - 1
resources/views/recipients/index.blade.php

@@ -4,6 +4,6 @@
     <div class="container py-8">
         @include('shared.status')
 
-        <recipients :user="{{json_encode(Auth::user())}}" :initial-recipients="{{json_encode($recipients)}}" :aliases-using-default="{{json_encode($aliasesUsingDefault)}}" domain="{{config('anonaddy.domain')}}" />
+        <recipients :user="{{json_encode(Auth::user())}}" :initial-recipients="{{json_encode($recipients)}}" :aliases-using-default="{{json_encode($aliasesUsingDefault)}}" :aliases-using-default-count="{{$aliasesUsingDefaultCount}}" domain="{{config('anonaddy.domain')}}" />
     </div>
 @endsection

+ 31 - 0
tests/Feature/Api/RecipientsTest.php

@@ -2,6 +2,7 @@
 
 namespace Tests\Feature\Api;
 
+use App\Models\Domain;
 use App\Models\Recipient;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Tests\TestCase;
@@ -108,6 +109,36 @@ class RecipientsTest extends TestCase
             ->assertJsonValidationErrors('email');
     }
 
+    /** @test */
+    public function user_can_not_create_recipient_with_local_domain()
+    {
+        $response = $this->json('POST', '/api/v1/recipients', [
+            'email' => 'johndoe@anonaddy.com'
+        ]);
+
+        $response
+            ->assertStatus(422)
+            ->assertJsonValidationErrors('email');
+    }
+
+    /** @test */
+    public function user_can_not_create_recipient_with_local_custom_domain()
+    {
+        Domain::factory()->create([
+            'user_id' => $this->user->id,
+            'domain' => 'example.com',
+            'domain_verified_at' => now()
+        ]);
+
+        $response = $this->json('POST', '/api/v1/recipients', [
+            'email' => 'johndoe@example.com'
+        ]);
+
+        $response
+            ->assertStatus(422)
+            ->assertJsonValidationErrors('email');
+    }
+
     /** @test */
     public function new_recipient_must_have_valid_email()
     {

+ 0 - 61
tests/Feature/ReceiveEmailTest.php

@@ -770,67 +770,6 @@ class ReceiveEmailTest extends TestCase
         });
     }
 
-    /** @test */
-    public function it_can_update_incorrect_aliasable_id_for_custom_domain()
-    {
-        Mail::fake();
-
-        Mail::assertNothingSent();
-
-        Subscription::factory()->create(['user_id' => $this->user->id]);
-
-        $domain = Domain::factory()->create([
-            'user_id' => $this->user->id,
-            'domain' => 'example.com',
-            'domain_verified_at' => now()
-        ]);
-
-        $domain->delete();
-
-        Alias::factory()->create([
-            'user_id' => $this->user->id,
-            'aliasable_id' => $domain->id,
-            'aliasable_type' => 'App\Models\Domain',
-            'email' => 'ebay@example.com',
-            'local_part' => 'ebay',
-            'domain' => 'example.com',
-        ]);
-
-        $newDomain = Domain::factory()->create([
-            'user_id' => $this->user->id,
-            'domain' => 'example.com',
-            'domain_verified_at' => now()
-        ]);
-
-        $this->artisan(
-            'anonaddy:receive-email',
-            [
-                'file' => base_path('tests/emails/email_custom_domain.eml'),
-                '--sender' => 'will@anonaddy.com',
-                '--recipient' => ['ebay@example.com'],
-                '--local_part' => ['ebay'],
-                '--extension' => [''],
-                '--domain' => ['example.com'],
-                '--size' => '871'
-            ]
-        )->assertExitCode(0);
-
-        $this->assertDatabaseHas('aliases', [
-            'aliasable_id' => $newDomain->id,
-            'aliasable_type' => 'App\Models\Domain',
-            'email' => 'ebay@example.com',
-            'local_part' => 'ebay',
-            'domain' => 'example.com',
-            'emails_blocked' => 0
-        ]);
-
-        $this->assertEquals(1, $this->user->aliases()->count());
-
-        Mail::assertQueued(ForwardEmail::class, function ($mail) {
-            return $mail->hasTo($this->user->email);
-        });
-    }
-
     /** @test */
     public function it_can_forward_email_for_custom_domain_with_verified_sending()
     {

+ 1 - 1
tests/Feature/ShowAdditionalUsernamesTest.php

@@ -8,7 +8,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Support\Carbon;
 use Tests\TestCase;
 
-class AdditionalUsernamesTest extends TestCase
+class ShowAdditionalUsernamesTest extends TestCase
 {
     use RefreshDatabase;
 

+ 1 - 1
tests/Feature/ShowAliasesTest.php

@@ -10,7 +10,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Support\Carbon;
 use Tests\TestCase;
 
-class AliasesTest extends TestCase
+class ShowAliasesTest extends TestCase
 {
     use RefreshDatabase;
 

+ 1 - 1
tests/Feature/ShowDomainsTest.php

@@ -8,7 +8,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Support\Carbon;
 use Tests\TestCase;
 
-class DomainsTest extends TestCase
+class ShowDomainsTest extends TestCase
 {
     use RefreshDatabase;
 

+ 2 - 1
tests/Feature/ShowRecipientsTest.php

@@ -14,7 +14,7 @@ use Illuminate\Support\Facades\Notification;
 use Illuminate\Support\Facades\URL;
 use Tests\TestCase;
 
-class RecipientsTest extends TestCase
+class ShowRecipientsTest extends TestCase
 {
     use RefreshDatabase;
 
@@ -31,6 +31,7 @@ class RecipientsTest extends TestCase
     /** @test */
     public function user_can_view_recipients_from_the_recipients_page()
     {
+        $this->withoutExceptionHandling();
         $recipients = Recipient::factory()->count(5)->create([
             'user_id' => $this->user->id
         ]);