浏览代码

Added domain verification

Will Browning 5 年之前
父节点
当前提交
7cda4f7a35

+ 40 - 1
app/Domain.php

@@ -5,6 +5,8 @@ namespace App;
 use App\Traits\HasEncryptedAttributes;
 use App\Traits\HasEncryptedAttributes;
 use App\Traits\HasUuid;
 use App\Traits\HasUuid;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
+use Spatie\Dns\Dns;
 
 
 class Domain extends Model
 class Domain extends Model
 {
 {
@@ -24,7 +26,8 @@ class Domain extends Model
 
 
     protected $dates = [
     protected $dates = [
         'created_at',
         'created_at',
-        'updated_at'
+        'updated_at',
+        'domain_verified_at'
     ];
     ];
 
 
     protected $casts = [
     protected $casts = [
@@ -72,4 +75,40 @@ class Domain extends Model
     {
     {
         $this->update(['active' => true]);
         $this->update(['active' => true]);
     }
     }
+
+    /**
+     * Determine if the domain is verified.
+     *
+     * @return bool
+     */
+    public function isVerified()
+    {
+        return ! is_null($this->domain_verified_at);
+    }
+
+    /**
+     * Mark this domain as verified.
+     *
+     * @return bool
+     */
+    public function markDomainAsVerified()
+    {
+        return $this->forceFill([
+            'domain_verified_at' => $this->freshTimestamp(),
+        ])->save();
+    }
+
+    /**
+     * Checks if the domain has the correct records.
+     *
+     * @return void
+     */
+    public function checkVerification()
+    {
+        $dns = new Dns($this->domain, '1.1.1.1');
+
+        if (Str::contains($dns->getRecords('MX'), 'MX 10 mail.anonaddy.me.')) {
+            $this->markDomainAsVerified();
+        }
+    }
 }
 }

+ 2 - 6
app/EmailData.php

@@ -11,15 +11,11 @@ class EmailData
         $this->sender = $parser->getAddresses('from')[0]['address'];
         $this->sender = $parser->getAddresses('from')[0]['address'];
         $this->display_from = $parser->getAddresses('from')[0]['display'];
         $this->display_from = $parser->getAddresses('from')[0]['display'];
         $this->subject = $parser->getHeader('subject');
         $this->subject = $parser->getHeader('subject');
-        $this->text = $parser->getMessageBody('text');
-        $this->html = $parser->getMessageBody('html');
+        $this->text = base64_encode($parser->getMessageBody('text'));
+        $this->html = base64_encode($parser->getMessageBody('html'));
         $this->attachments = [];
         $this->attachments = [];
 
 
         foreach ($parser->getAttachments() as $attachment) {
         foreach ($parser->getAttachments() as $attachment) {
-            if ($attachment->getContentType() === 'text/plain') {
-                $this->text = base64_encode($parser->getMessageBody('text'));
-            }
-
             $this->attachments[] = [
             $this->attachments[] = [
               'stream' => base64_encode(stream_get_contents($attachment->getStream())),
               'stream' => base64_encode(stream_get_contents($attachment->getStream())),
               'file_name' => $attachment->getFileName(),
               'file_name' => $attachment->getFileName(),

+ 2 - 0
app/Http/Controllers/DomainController.php

@@ -19,6 +19,8 @@ class DomainController extends Controller
     {
     {
         $domain = user()->domains()->create(['domain' => $request->domain]);
         $domain = user()->domains()->create(['domain' => $request->domain]);
 
 
+        $domain->checkVerification();
+
         return new DomainResource($domain->fresh());
         return new DomainResource($domain->fresh());
     }
     }
 
 

+ 26 - 0
app/Http/Controllers/DomainVerificationController.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Resources\DomainResource;
+
+class DomainVerificationController extends Controller
+{
+    public function __construct()
+    {
+        $this->middleware('throttle:1,1');
+    }
+
+    public function recheck($id)
+    {
+        $domain = user()->domains()->findOrFail($id);
+
+        if ($domain->isVerified()) {
+            return response('Domain already verified', 404);
+        }
+
+        $domain->checkVerification();
+
+        return new DomainResource($domain->fresh());
+    }
+}

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

@@ -15,6 +15,7 @@ class DomainResource extends JsonResource
             'description' => $this->description,
             'description' => $this->description,
             'aliases' => $this->aliases,
             'aliases' => $this->aliases,
             'active' => $this->active,
             'active' => $this->active,
+            'domain_verified_at' => $this->domain_verified_at ? $this->domain_verified_at->toDateTimeString() : null,
             'created_at' => $this->created_at->toDateTimeString(),
             'created_at' => $this->created_at->toDateTimeString(),
             'updated_at' => $this->updated_at->toDateTimeString(),
             'updated_at' => $this->updated_at->toDateTimeString(),
         ];
         ];

+ 2 - 2
app/Mail/ForwardEmail.php

@@ -66,7 +66,7 @@ class ForwardEmail extends Mailable implements ShouldQueue
             ->replyTo($replyToEmail, $this->sender)
             ->replyTo($replyToEmail, $this->sender)
             ->subject($this->user->email_subject ?? $this->emailSubject)
             ->subject($this->user->email_subject ?? $this->emailSubject)
             ->text('emails.forward.text')->with([
             ->text('emails.forward.text')->with([
-                'text' => $this->emailText
+                'text' => base64_decode($this->emailText)
             ])
             ])
             ->with([
             ->with([
                 'location' => $this->bannerLocation,
                 'location' => $this->bannerLocation,
@@ -89,7 +89,7 @@ class ForwardEmail extends Mailable implements ShouldQueue
 
 
         if ($this->emailHtml) {
         if ($this->emailHtml) {
             $email->view('emails.forward.html')->with([
             $email->view('emails.forward.html')->with([
-                'html' => $this->emailHtml
+                'html' => base64_decode($this->emailHtml)
             ]);
             ]);
         }
         }
 
 

+ 2 - 2
app/Mail/ReplyToEmail.php

@@ -50,7 +50,7 @@ class ReplyToEmail extends Mailable implements ShouldQueue
             ->replyTo($this->alias->email, $fromName)
             ->replyTo($this->alias->email, $fromName)
             ->subject($this->emailSubject)
             ->subject($this->emailSubject)
             ->text('emails.reply.text')->with([
             ->text('emails.reply.text')->with([
-                'text' => $this->emailText
+                'text' => base64_decode($this->emailText)
             ])
             ])
             ->withSwiftMessage(function ($message) {
             ->withSwiftMessage(function ($message) {
                 $message->getHeaders()
                 $message->getHeaders()
@@ -59,7 +59,7 @@ class ReplyToEmail extends Mailable implements ShouldQueue
 
 
         if ($this->emailHtml) {
         if ($this->emailHtml) {
             $email->view('emails.reply.html')->with([
             $email->view('emails.reply.html')->with([
-                'html' => $this->emailHtml
+                'html' => base64_decode($this->emailHtml)
             ]);
             ]);
         }
         }
 
 

+ 2 - 1
composer.json

@@ -18,7 +18,8 @@
         "php-mime-mail-parser/php-mime-mail-parser": "^5.0",
         "php-mime-mail-parser/php-mime-mail-parser": "^5.0",
         "pragmarx/google2fa-laravel": "^1.0",
         "pragmarx/google2fa-laravel": "^1.0",
         "predis/predis": "^1.1",
         "predis/predis": "^1.1",
-        "ramsey/uuid": "^3.8"
+        "ramsey/uuid": "^3.8",
+        "spatie/dns": "^1.4"
     },
     },
     "require-dev": {
     "require-dev": {
         "beyondcode/laravel-dump-server": "^1.0",
         "beyondcode/laravel-dump-server": "^1.0",

+ 54 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "content-hash": "3ab9e7ebe3aa110aaedb169c102eb527",
+    "content-hash": "4e49b7e999da1d1b9491f4017efc9d51",
     "packages": [
     "packages": [
         {
         {
             "name": "bacon/bacon-qr-code",
             "name": "bacon/bacon-qr-code",
@@ -2413,6 +2413,59 @@
             ],
             ],
             "time": "2018-07-19T23:38:55+00:00"
             "time": "2018-07-19T23:38:55+00:00"
         },
         },
+        {
+            "name": "spatie/dns",
+            "version": "1.4.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/spatie/dns.git",
+                "reference": "403865f19c01e0b9cfa9356fba5748dd5307b35c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/spatie/dns/zipball/403865f19c01e0b9cfa9356fba5748dd5307b35c",
+                "reference": "403865f19c01e0b9cfa9356fba5748dd5307b35c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1",
+                "symfony/process": "^4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Spatie\\Dns\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Freek Van der Herten",
+                    "role": "Developer",
+                    "email": "freek@spatie.be",
+                    "homepage": "https://spatie.be"
+                },
+                {
+                    "name": "Harish Toshniwal",
+                    "role": "Developer",
+                    "email": "harish@spatie.be",
+                    "homepage": "https://spatie.be"
+                }
+            ],
+            "description": "Retrieve DNS records",
+            "homepage": "https://github.com/spatie/dns",
+            "keywords": [
+                "dns",
+                "spatie"
+            ],
+            "time": "2019-05-17T07:19:43+00:00"
+        },
         {
         {
             "name": "swiftmailer/swiftmailer",
             "name": "swiftmailer/swiftmailer",
             "version": "v6.2.1",
             "version": "v6.2.1",

+ 32 - 0
database/migrations/2019_08_21_104530_add_domain_verified_at_to_domains_table.php

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

+ 78 - 3
resources/js/pages/Domains.vue

@@ -98,7 +98,7 @@
               </div>
               </div>
             </div>
             </div>
           </th>
           </th>
-          <th class="p-4 items-center" colspan="2">
+          <th class="p-4 items-center">
             <div class="flex items-center">
             <div class="flex items-center">
               Active
               Active
               <div class="inline-flex flex-col">
               <div class="inline-flex flex-col">
@@ -117,6 +117,29 @@
               </div>
               </div>
             </div>
             </div>
           </th>
           </th>
+          <th class="p-4" colspan="2">
+            <div class="flex items-center">
+              Verified
+              <div class="inline-flex flex-col">
+                <icon
+                  name="chevron-up"
+                  @click.native="sort('domain_verified_at', 'asc')"
+                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
+                  :class="{
+                    'text-grey-800': isCurrentSort('domain_verified_at', 'asc'),
+                  }"
+                />
+                <icon
+                  name="chevron-down"
+                  @click.native="sort('domain_verified_at', 'desc')"
+                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
+                  :class="{
+                    'text-grey-800': isCurrentSort('domain_verified_at', 'desc'),
+                  }"
+                />
+              </div>
+            </div>
+          </th>
         </tr>
         </tr>
         <tr
         <tr
           v-for="domain in queriedDomains"
           v-for="domain in queriedDomains"
@@ -209,6 +232,26 @@
               />
               />
             </div>
             </div>
           </td>
           </td>
+          <td class="border-grey-200 border-t">
+            <div class="p-4 flex items-center focus:text-indigo-500 text-sm">
+              <span
+                name="check"
+                v-if="domain.domain_verified_at"
+                class="py-1 px-2 bg-green-200 text-green-900 rounded-full"
+              >
+                verified
+              </span>
+              <button
+                v-else
+                @click="recheckRecords(domain)"
+                class="focus:outline-none"
+                :class="recheckRecordsLoading ? 'cursor-not-allowed' : ''"
+                :disabled="recheckRecordsLoading"
+              >
+                Recheck domain
+              </button>
+            </div>
+          </td>
           <td class="border-grey-200 border-t w-px">
           <td class="border-grey-200 border-t w-px">
             <div
             <div
               class="px-4 flex items-center cursor-pointer outline-none focus:text-indigo-500"
               class="px-4 flex items-center cursor-pointer outline-none focus:text-indigo-500"
@@ -222,7 +265,7 @@
         <tr v-if="queriedDomains.length === 0">
         <tr v-if="queriedDomains.length === 0">
           <td
           <td
             class="border-grey-200 border-t p-4 text-center h-24 text-lg text-grey-700"
             class="border-grey-200 border-t p-4 text-center h-24 text-lg text-grey-700"
-            colspan="5"
+            colspan="6"
           >
           >
             No domains found for that search!
             No domains found for that search!
           </td>
           </td>
@@ -362,6 +405,7 @@ export default {
       domainDescriptionToEdit: '',
       domainDescriptionToEdit: '',
       deleteDomainLoading: false,
       deleteDomainLoading: false,
       deleteDomainModalOpen: false,
       deleteDomainModalOpen: false,
+      recheckRecordsLoading: false,
       currentSort: 'created_at',
       currentSort: 'created_at',
       currentSortDir: 'desc',
       currentSortDir: 'desc',
       errors: {},
       errors: {},
@@ -428,13 +472,37 @@ export default {
         })
         })
         .catch(error => {
         .catch(error => {
           this.addDomainLoading = false
           this.addDomainLoading = false
-          if (error.response.status == 422) {
+          if (error.response.status === 422) {
             this.error(error.response.data.errors.domain[0])
             this.error(error.response.data.errors.domain[0])
           } else {
           } else {
             this.error()
             this.error()
           }
           }
         })
         })
     },
     },
+    recheckRecords(domain) {
+      this.recheckRecordsLoading = true
+
+      axios
+        .get(`/domains/${domain.id}/recheck`)
+        .then(({ data }) => {
+          this.recheckRecordsLoading = false
+
+          if (data.data.domain_verified_at === null) {
+            this.warn('MX record not found, please try again later')
+          } else {
+            this.success('Domain verified successfully')
+            domain.domain_verified_at = data.data.domain_verified_at
+          }
+        })
+        .catch(error => {
+          this.recheckRecordsLoading = false
+          if (error.response.status === 429) {
+            this.error('You can only recheck the records once a minute')
+          } else {
+            this.error()
+          }
+        })
+    },
     openDeleteModal(id) {
     openDeleteModal(id) {
       this.deleteDomainModalOpen = true
       this.deleteDomainModalOpen = true
       this.domainIdToDelete = id
       this.domainIdToDelete = id
@@ -539,6 +607,13 @@ export default {
     clipboardError() {
     clipboardError() {
       this.error('Could not copy to clipboard')
       this.error('Could not copy to clipboard')
     },
     },
+    warn(text = '') {
+      this.$notify({
+        title: 'Information',
+        text: text,
+        type: 'warn',
+      })
+    },
     success(text = '') {
     success(text = '') {
       this.$notify({
       this.$notify({
         title: 'Success',
         title: 'Success',

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

@@ -513,7 +513,7 @@ export default {
         })
         })
         .catch(error => {
         .catch(error => {
           this.addRecipientLoading = false
           this.addRecipientLoading = false
-          if (error.response.status == 422) {
+          if (error.response.status === 422) {
             this.error(error.response.data.errors.email[0])
             this.error(error.response.data.errors.email[0])
           } else {
           } else {
             this.error()
             this.error()

+ 4 - 2
routes/web.php

@@ -28,6 +28,8 @@ Route::middleware(['auth', 'verified', '2fa'])->group(function () {
     Route::post('/recipients', 'RecipientController@store')->name('recipients.store');
     Route::post('/recipients', 'RecipientController@store')->name('recipients.store');
     Route::delete('/recipients/{id}', 'RecipientController@destroy')->name('recipients.destroy');
     Route::delete('/recipients/{id}', 'RecipientController@destroy')->name('recipients.destroy');
 
 
+    Route::get('/recipients/{id}/email/resend', 'RecipientVerificationController@resend')->name('recipient_verification.resend');
+
     Route::patch('/recipient-keys/{id}', 'RecipientKeyController@update')->name('recipient_keys.update');
     Route::patch('/recipient-keys/{id}', 'RecipientKeyController@update')->name('recipient_keys.update');
     Route::delete('/recipient-keys/{id}', 'RecipientKeyController@destroy')->name('recipient_keys.destroy');
     Route::delete('/recipient-keys/{id}', 'RecipientKeyController@destroy')->name('recipient_keys.destroy');
 
 
@@ -36,13 +38,13 @@ Route::middleware(['auth', 'verified', '2fa'])->group(function () {
 
 
     Route::post('/alias-recipients', 'AliasRecipientController@store')->name('alias_recipients.store');
     Route::post('/alias-recipients', 'AliasRecipientController@store')->name('alias_recipients.store');
 
 
-    Route::get('/recipients/{id}/email/resend', 'RecipientVerificationController@resend')->name('recipient_verification.resend');
-
     Route::get('/domains', 'DomainController@index')->name('domains.index');
     Route::get('/domains', 'DomainController@index')->name('domains.index');
     Route::post('/domains', 'DomainController@store')->name('domains.store');
     Route::post('/domains', 'DomainController@store')->name('domains.store');
     Route::patch('/domains/{id}', 'DomainController@update')->name('domains.update');
     Route::patch('/domains/{id}', 'DomainController@update')->name('domains.update');
     Route::delete('/domains/{id}', 'DomainController@destroy')->name('domains.destroy');
     Route::delete('/domains/{id}', 'DomainController@destroy')->name('domains.destroy');
 
 
+    Route::get('/domains/{id}/recheck', 'DomainVerificationController@recheck')->name('domain_verification.recheck');
+
     Route::post('/active-domains', 'ActiveDomainController@store')->name('active_domains.store');
     Route::post('/active-domains', 'ActiveDomainController@store')->name('active_domains.store');
     Route::delete('/active-domains/{id}', 'ActiveDomainController@destroy')->name('active_domains.destroy');
     Route::delete('/active-domains/{id}', 'ActiveDomainController@destroy')->name('active_domains.destroy');
 
 

+ 19 - 0
tests/Feature/DomainsTest.php

@@ -137,6 +137,25 @@ class DomainsTest extends TestCase
             ->assertJsonValidationErrors('domain');
             ->assertJsonValidationErrors('domain');
     }
     }
 
 
+    /** @test */
+    public function user_can_verify_domain_records()
+    {
+        $domain = factory(Domain::class)->create([
+            'user_id' => $this->user->id,
+            'domain' => 'anonaddy.me'
+        ]);
+
+        $response = $this->get('/domains/'.$domain->id.'/recheck');
+
+        $response->assertStatus(200);
+
+        $this->assertDatabaseHas('domains', [
+            'user_id' => $this->user->id,
+            'domain' => 'anonaddy.me',
+            'domain_verified_at' => now()
+        ]);
+    }
+
     /** @test */
     /** @test */
     public function user_can_activate_domain()
     public function user_can_activate_domain()
     {
     {