Bläddra i källkod

Updated custom domain verification

Will Browning 5 år sedan
förälder
incheckning
9a48650ae2

+ 6 - 18
.env.example

@@ -1,4 +1,4 @@
-APP_NAME=Example
+APP_NAME=AnonAddy
 APP_ENV=local
 APP_KEY=
 APP_DEBUG=true
@@ -17,31 +17,17 @@ BROADCAST_DRIVER=log
 CACHE_DRIVER=file
 QUEUE_CONNECTION=sync
 SESSION_DRIVER=file
-SESSION_LIFETIME=120
+SESSION_LIFETIME=10080
 
 REDIS_HOST=127.0.0.1
 REDIS_PASSWORD=null
 REDIS_PORT=6379
 
-MAIL_DRIVER=smtp
-MAIL_HOST=smtp.mailtrap.io
-MAIL_PORT=2525
-MAIL_USERNAME=null
-MAIL_PASSWORD=null
-MAIL_ENCRYPTION=null
-
+MAIL_DRIVER=sendmail
 MAIL_FROM_NAME=Example
 MAIL_FROM_ADDRESS=mailer@example.com
 
-PUSHER_APP_ID=
-PUSHER_APP_KEY=
-PUSHER_APP_SECRET=
-PUSHER_APP_CLUSTER=mt1
-
-MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
-MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
-
-ANONADDY_RETURN_PATH=bounces@example.com
+ANONADDY_RETURN_PATH=mailer@example.com
 ANONADDY_ADMIN_USERNAME=johndoe
 ANONADDY_ENABLE_REGISTRATION=true
 ANONADDY_DOMAIN=example.com
@@ -54,3 +40,5 @@ ANONADDY_BANDWIDTH_LIMIT=104857600
 ANONADDY_NEW_ALIAS_LIMIT=10
 ANONADDY_ADDITIONAL_USERNAME_LIMIT=3
 ANONADDY_SIGNING_KEY_FINGERPRINT=your-signing-key-fingerprint
+ANONADDY_DKIM_SIGNING_KEY=/path/to/your/dkim/signing/key
+ANONADDY_DKIM_SELECTOR=default

+ 2 - 2
README.md

@@ -228,8 +228,8 @@ For any other questions just send an email to - [contact@anonaddy.com](mailto:co
 
 #### Prerequisites
 
-* Postfix (3.0.0+) (plus postfix-mysql for database queries)
-* PHP (7.2+) and the [php-mailparse](https://pecl.php.net/package/mailparse) extension plus the [php-gnupg](https://pecl.php.net/package/gnupg) extension if you plan to encrypt forwarded emails
+* Postfix (3.0.0+) (plus postfix-mysql for database queries and postfix-pcre)
+* PHP (7.3+) and the [php-mailparse](https://pecl.php.net/package/mailparse) extension, the [php-gnupg](https://pecl.php.net/package/gnupg) extension if you plan to encrypt forwarded emails, the [php-imagick](https://pecl.php.net/package/imagick) extension for generating 2FA QR codes
 * Port 25 unblocked and open
 * Redis (4.x+) for throttling and queues
 * FQDN as hostname e.g. mail.anonaddy.me

+ 10 - 0
app/Alias.php

@@ -6,6 +6,7 @@ use App\Traits\HasEncryptedAttributes;
 use App\Traits\HasUuid;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Str;
 
 class Alias extends Model
 {
@@ -135,4 +136,13 @@ class Alias extends Model
     {
         return $this->aliasable_type === 'App\Domain';
     }
+
+    public function parentDomain()
+    {
+        return collect(config('anonaddy.all_domains'))
+            ->filter(function ($name) {
+                return Str::endsWith($this->domain, $name);
+            })
+            ->first();
+    }
 }

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

@@ -287,7 +287,7 @@ class ReceiveEmail extends Command
                 } catch (\Exception $e) {
                     report($e);
 
-                    $part['headers']['from'] = str_replace("\\\"", "", $part['headers']['from']);
+                    $part['headers']['from'] = str_replace("\\", "", $part['headers']['from']);
                     $mimePart->setPart($part);
                 }
             }

+ 78 - 15
app/Domain.php

@@ -6,6 +6,7 @@ use App\Http\Resources\DomainResource;
 use App\Traits\HasEncryptedAttributes;
 use App\Traits\HasUuid;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\App;
 
 class Domain extends Model
 {
@@ -28,7 +29,8 @@ class Domain extends Model
     protected $dates = [
         'created_at',
         'updated_at',
-        'domain_verified_at'
+        'domain_verified_at',
+        'domain_sending_verified_at'
     ];
 
     protected $casts = [
@@ -105,6 +107,16 @@ class Domain extends Model
         return ! is_null($this->domain_verified_at);
     }
 
+    /**
+     * Determine if the domain is verified for sending.
+     *
+     * @return bool
+     */
+    public function isVerifiedForSending()
+    {
+        return ! is_null($this->domain_sending_verified_at);
+    }
+
     /**
      * Mark this domain as verified.
      *
@@ -118,36 +130,87 @@ class Domain extends Model
     }
 
     /**
-     * Checks if the domain has the correct records.
+     * Mark this domain as verified for sending.
      *
-     * @return void
+     * @return bool
+     */
+    public function markDomainAsVerifiedForSending()
+    {
+        return $this->forceFill([
+            'domain_sending_verified_at' => $this->freshTimestamp(),
+        ])->save();
+    }
+
+    /**
+     * Checks if the domain has the correct records.
      */
     public function checkVerification()
     {
-        $records = collect(dns_get_record($this->domain . '.', DNS_MX));
+        return collect(dns_get_record($this->domain . '.', DNS_TXT))
+            ->contains(function ($r) {
+                return $r['txt'] === 'aa-verify=' . sha1(config('anonaddy.secret') . user()->id) || App::environment('testing');
+            });
+    }
+
+    /**
+     * Checks if the domain has the correct records for sending.
+     */
+    public function checkVerificationForSending()
+    {
+        $spf = collect(dns_get_record($this->domain . '.', DNS_TXT))
+            ->contains(function ($r) {
+                return preg_match("/^(v=spf1).*(include:spf\." . config('anonaddy.domain') . ").*(-|~)all$/", $r['txt']);
+            });
+
+        if (!$spf) {
+            return response()->json([
+                'success' => false,
+                'message' => 'SPF record not found. This could be due to DNS caching, please try again later.'
+            ]);
+        }
 
-        $lowestPriority = $records->groupBy('pri')->sortKeys()->first();
+        $dmarc = collect(dns_get_record('_dmarc.' . $this->domain . '.', DNS_TXT))
+            ->contains(function ($r) {
+                return preg_match("/^(v=DMARC1).*(p=quarantine|reject).*/", $r['txt']);
+            });
 
-        if ($lowestPriority->count() !== 1) {
+        if (!$dmarc) {
             return response()->json([
                 'success' => false,
-                'message' => 'Please make sure you do not have any other MX records with the same priority.'
+                'message' => 'DMARC record not found. This could be due to DNS caching, please try again later.'
             ]);
         }
 
-        // Check the target for the lowest priority record is correct.
-        if ($lowestPriority->first()['target'] === 'mail.anonaddy.me') {
-            $this->markDomainAsVerified();
+        $dk1 = collect(dns_get_record('dk1._domainkey.' . $this->domain . '.', DNS_CNAME))
+            ->contains(function ($r) {
+                return $r['target'] === 'dk1._domainkey.' . config('anonaddy.domain');
+            });
+
+        if (!$dk1) {
             return response()->json([
-                'success' => true,
-                'message' => 'MX Record successfully verified.',
-                'data' => new DomainResource($this->fresh())
+                'success' => false,
+                'message' => 'CNAME dk1._domainkey record not found. This could be due to DNS caching, please try again later.'
             ]);
         }
 
+        $dk2 = collect(dns_get_record('dk2._domainkey.' . $this->domain . '.', DNS_CNAME))
+            ->contains(function ($r) {
+                return $r['target'] === 'dk2._domainkey.' . config('anonaddy.domain');
+            });
+
+        if (!$dk2) {
+            return response()->json([
+                'success' => false,
+                'message' => 'CNAME dk2._domainkey record not found. This could be due to DNS caching, please try again later.'
+            ]);
+        }
+
+        $this->markDomainAsVerifiedForSending();
+
         return response()->json([
-            'success' => false,
-            'message' => 'Record not found. This could be due to DNS caching, please try again later.'
+            'success' => true,
+            'message' => 'Records successfully verified for sending.',
+            'data' => new DomainResource($this->fresh())
         ]);
     }
 }

+ 15 - 2
app/Http/Controllers/Api/DomainController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers\Api;
 
+use App\Domain;
 use App\Http\Controllers\Controller;
 use App\Http\Requests\StoreDomainRequest;
 use App\Http\Requests\UpdateDomainRequest;
@@ -9,6 +10,11 @@ use App\Http\Resources\DomainResource;
 
 class DomainController extends Controller
 {
+    public function __construct()
+    {
+        $this->middleware('throttle:6,1')->only('store');
+    }
+
     public function index()
     {
         return DomainResource::collection(user()->domains()->with(['aliases', 'defaultRecipient'])->latest()->get());
@@ -23,9 +29,16 @@ class DomainController extends Controller
 
     public function store(StoreDomainRequest $request)
     {
-        $domain = user()->domains()->create(['domain' => $request->domain]);
+        $domain = new Domain();
+        $domain->domain = $request->domain;
+
+        if (! $domain->checkVerification()) {
+            return response('Verification record not found, please add the following TXT record to your domain: aa-verify=' . sha1(config('anonaddy.secret') . user()->id), 404);
+        }
+
+        user()->domains()->save($domain);
 
-        $domain->checkVerification();
+        $domain->markDomainAsVerified();
 
         return new DomainResource($domain->refresh()->load(['aliases', 'defaultRecipient']));
     }

+ 5 - 0
app/Http/Controllers/Api/RecipientKeyController.php

@@ -39,6 +39,11 @@ class RecipientKeyController extends Controller
 
         user()->deleteKeyFromKeyring($recipient->fingerprint);
 
+        $recipient->update([
+            'should_encrypt' => false,
+            'fingerprint' => null
+        ]);
+
         return response('', 204);
     }
 }

+ 3 - 1
app/Http/Controllers/Auth/RegisterController.php

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
 use App\Recipient;
 use App\Rules\NotBlacklisted;
 use App\Rules\NotDeletedUsername;
+use App\Rules\NotLocalRecipient;
 use App\Rules\RegisterUniqueRecipient;
 use App\User;
 use Illuminate\Foundation\Auth\RegistersUsers;
@@ -69,7 +70,8 @@ class RegisterController extends Controller
                 'email:rfc,dns',
                 'max:254',
                 'confirmed',
-                new RegisterUniqueRecipient
+                new RegisterUniqueRecipient,
+                new NotLocalRecipient
             ],
             'password' => ['required', 'min:8'],
         ], [

+ 5 - 5
app/Http/Controllers/DomainVerificationController.php

@@ -6,17 +6,17 @@ class DomainVerificationController extends Controller
 {
     public function __construct()
     {
-        $this->middleware('throttle:1,1');
+        $this->middleware('throttle:6,1');
     }
 
-    public function recheck($id)
+    public function checkSending($id)
     {
         $domain = user()->domains()->findOrFail($id);
 
-        if ($domain->isVerified()) {
-            return response('Domain already verified', 404);
+        if ($domain->isVerifiedForSending()) {
+            return response('Domain already verified for sending', 404);
         }
 
-        return $domain->checkVerification();
+        return $domain->checkVerificationForSending();
     }
 }

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

@@ -2,6 +2,7 @@
 
 namespace App\Http\Requests;
 
+use App\Rules\NotLocalRecipient;
 use App\Rules\UniqueRecipient;
 use Illuminate\Foundation\Http\FormRequest;
 
@@ -30,7 +31,8 @@ class StoreRecipientRequest extends FormRequest
                 'string',
                 'max:254',
                 'email:rfc,dns',
-                new UniqueRecipient
+                new UniqueRecipient,
+                new NotLocalRecipient
             ]
         ];
     }

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

@@ -17,6 +17,7 @@ class DomainResource extends JsonResource
             'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
             'active' => $this->active,
             'domain_verified_at' => $this->domain_verified_at ? $this->domain_verified_at->toDateTimeString() : null,
+            'domain_sending_verified_at' => $this->domain_sending_verified_at ? $this->domain_sending_verified_at->toDateTimeString() : null,
             'created_at' => $this->created_at->toDateTimeString(),
             'updated_at' => $this->updated_at->toDateTimeString(),
         ];

+ 25 - 3
app/Mail/ForwardEmail.php

@@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Mail\Mailable;
 use Illuminate\Queue\SerializesModels;
 use Illuminate\Support\Facades\URL;
+use Swift_Signers_DKIMSigner;
 
 class ForwardEmail extends Mailable implements ShouldQueue
 {
@@ -28,6 +29,7 @@ class ForwardEmail extends Mailable implements ShouldQueue
     protected $bannerLocation;
     protected $fingerprint;
     protected $openpgpsigner;
+    protected $dkimSigner;
 
     /**
      * Create a new message instance.
@@ -64,7 +66,23 @@ class ForwardEmail extends Mailable implements ShouldQueue
         $replyToDisplay = $this->replyToAddress ?? $this->sender;
         $replyToEmail = $this->alias->local_part.'+'.sha1(config('anonaddy.secret').$replyToDisplay).'@'.$this->alias->domain;
 
-        $fromEmail = $this->alias->isUuid() ? $this->alias->email : config('mail.from.address');
+        if ($this->alias->isCustomDomain()) {
+            if ($this->alias->aliasable->isVerifiedForSending()) {
+                $fromEmail = $this->alias->email;
+                $returnPath = $this->alias->email;
+
+                $this->dkimSigner = new Swift_Signers_DKIMSigner(config('anonaddy.dkim_signing_key'), $this->alias->domain, config('anonaddy.dkim_selector'));
+                $this->dkimSigner->ignoreHeader('List-Unsubscribe');
+                $this->dkimSigner->ignoreHeader('Return-Path');
+                $this->dkimSigner->setBodyCanon('relaxed');
+            } else {
+                $fromEmail = config('mail.from.address');
+                $returnPath = config('anonaddy.return_path');
+            }
+        } else {
+            $fromEmail = $this->alias->email;
+            $returnPath = 'mailer@'.$this->alias->parentDomain();
+        }
 
         $email =  $this
             ->from($fromEmail, base64_decode($this->displayFrom)." '".$this->sender."'")
@@ -80,18 +98,22 @@ class ForwardEmail extends Mailable implements ShouldQueue
                 'fromEmail' => $this->sender,
                 'replacedSubject' => $this->user->email_subject ? ' with subject "' . base64_decode($this->emailSubject) . '"' : null
             ])
-            ->withSwiftMessage(function ($message) {
+            ->withSwiftMessage(function ($message) use ($returnPath) {
                 $message->getHeaders()
                         ->addTextHeader('List-Unsubscribe', '<mailto:' . $this->alias->id . '@unsubscribe.' . config('anonaddy.domain') . '?subject=unsubscribe>, <' . $this->deactivateUrl . '>');
 
                 $message->getHeaders()
-                        ->addTextHeader('Return-Path', config('anonaddy.return_path'));
+                        ->addTextHeader('Return-Path', $returnPath);
 
                 $message->setId(bin2hex(random_bytes(16)).'@'.$this->alias->domain);
 
                 if ($this->fingerprint) {
                     $message->attachSigner($this->openpgpsigner);
                 }
+
+                if ($this->dkimSigner) {
+                    $message->attachSigner($this->dkimSigner);
+                }
             });
 
         if ($this->emailHtml) {

+ 24 - 2
app/Mail/ReplyToEmail.php

@@ -9,6 +9,7 @@ use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Mail\Mailable;
 use Illuminate\Queue\SerializesModels;
+use Swift_Signers_DKIMSigner;
 
 class ReplyToEmail extends Mailable implements ShouldQueue
 {
@@ -20,6 +21,7 @@ class ReplyToEmail extends Mailable implements ShouldQueue
     protected $emailText;
     protected $emailHtml;
     protected $emailAttachments;
+    protected $dkimSigner;
 
     /**
      * Create a new message instance.
@@ -44,7 +46,23 @@ class ReplyToEmail extends Mailable implements ShouldQueue
     public function build()
     {
         $fromName = $this->user->from_name ? $this->user->from_name : $this->alias->email;
-        $fromEmail = $this->alias->isUuid() ? $this->alias->email : config('mail.from.address');
+
+        if ($this->alias->isCustomDomain()) {
+            if ($this->alias->aliasable->isVerifiedForSending()) {
+                $fromEmail = $this->alias->email;
+                $returnPath = $this->alias->email;
+
+                $this->dkimSigner = new Swift_Signers_DKIMSigner(config('anonaddy.dkim_signing_key'), $this->alias->domain, config('anonaddy.dkim_selector'));
+                $this->dkimSigner->ignoreHeader('Return-Path');
+                $this->dkimSigner->setBodyCanon('relaxed');
+            } else {
+                $fromEmail = config('mail.from.address');
+                $returnPath = config('anonaddy.return_path');
+            }
+        } else {
+            $fromEmail = $this->alias->email;
+            $returnPath = 'mailer@'.$this->alias->parentDomain();
+        }
 
         $email =  $this
             ->from($fromEmail, $fromName)
@@ -52,11 +70,15 @@ class ReplyToEmail extends Mailable implements ShouldQueue
             ->text('emails.reply.text')->with([
                 'text' => base64_decode($this->emailText)
             ])
-            ->withSwiftMessage(function ($message) {
+            ->withSwiftMessage(function ($message) use ($returnPath) {
                 $message->getHeaders()
                         ->addTextHeader('Return-Path', config('anonaddy.return_path'));
 
                 $message->setId(bin2hex(random_bytes(16)).'@'.$this->alias->domain);
+
+                if ($this->dkimSigner) {
+                    $message->attachSigner($this->dkimSigner);
+                }
             });
 
         if (! $this->alias->isUuid()) {

+ 49 - 0
app/Rules/NotLocalRecipient.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Rules;
+
+use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Support\Str;
+
+class NotLocalRecipient 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)
+    {
+        $emailDomain = Str::afterLast($value, '@');
+
+        $count = collect(config('anonaddy.all_domains'))
+            ->filter(function ($domain) use ($emailDomain) {
+                return Str::endsWith(strtolower($emailDomain), $domain);
+            })
+            ->count();
+
+        return $count === 0;
+    }
+
+    /**
+     * Get the validation error message.
+     *
+     * @return string
+     */
+    public function message()
+    {
+        return 'The recipient cannot be a local one or alias.';
+    }
+}

+ 12 - 10
app/User.php

@@ -275,15 +275,17 @@ class User extends Authenticatable implements MustVerifyEmail
             });
 
         // Remove the key from all user recipients using that same fingerprint.
-        $this
-            ->recipients()
-            ->get()
-            ->where('fingerprint', $fingerprint)
-            ->each(function ($recipient) {
-                $recipient->update([
-                    'should_encrypt' => false,
-                    'fingerprint' => null
-                ]);
-            });
+        if (! $gnupg->keyinfo($fingerprint)) {
+            $this
+                ->recipients()
+                ->get()
+                ->where('fingerprint', $fingerprint)
+                ->each(function ($recipient) {
+                    $recipient->update([
+                        'should_encrypt' => false,
+                        'fingerprint' => null
+                    ]);
+                });
+        }
     }
 }

+ 155 - 150
composer.lock

@@ -162,25 +162,25 @@
         },
         {
             "name": "dnoegel/php-xdg-base-dir",
-            "version": "0.1",
+            "version": "v0.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/dnoegel/php-xdg-base-dir.git",
-                "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a"
+                "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/265b8593498b997dc2d31e75b89f053b5cc9621a",
-                "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a",
+                "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
+                "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "@stable"
+                "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35"
             },
-            "type": "project",
+            "type": "library",
             "autoload": {
                 "psr-4": {
                     "XdgBaseDir\\": "src/"
@@ -191,7 +191,7 @@
                 "MIT"
             ],
             "description": "implementation of xdg base directory specification for php",
-            "time": "2014-10-24T07:27:01+00:00"
+            "time": "2019-12-04T15:06:13+00:00"
         },
         {
             "name": "doctrine/cache",
@@ -832,16 +832,16 @@
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.4.1",
+            "version": "6.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "0895c932405407fd3a7368b6910c09a24d26db11"
+                "reference": "dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0895c932405407fd3a7368b6910c09a24d26db11",
-                "reference": "0895c932405407fd3a7368b6910c09a24d26db11",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5",
+                "reference": "dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5",
                 "shasum": ""
             },
             "require": {
@@ -856,12 +856,13 @@
                 "psr/log": "^1.1"
             },
             "suggest": {
+                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                 "psr/log": "Required for using the Log middleware"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.3-dev"
+                    "dev-master": "6.5-dev"
                 }
             },
             "autoload": {
@@ -894,7 +895,7 @@
                 "rest",
                 "web service"
             ],
-            "time": "2019-10-23T15:58:00+00:00"
+            "time": "2019-12-07T18:20:45+00:00"
         },
         {
             "name": "guzzlehttp/promises",
@@ -1178,16 +1179,16 @@
         },
         {
             "name": "laravel/framework",
-            "version": "v6.6.1",
+            "version": "v6.9.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "0a8e3e6e96e5ec3af6b9f30026cb8e5551554875"
+                "reference": "60610be97ca389fa4b959d4d13fb3690970d9fb7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/0a8e3e6e96e5ec3af6b9f30026cb8e5551554875",
-                "reference": "0a8e3e6e96e5ec3af6b9f30026cb8e5551554875",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/60610be97ca389fa4b959d4d13fb3690970d9fb7",
+                "reference": "60610be97ca389fa4b959d4d13fb3690970d9fb7",
                 "shasum": ""
             },
             "require": {
@@ -1283,7 +1284,7 @@
                 "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).",
                 "moontoast/math": "Required to use ordered UUIDs (^1.1).",
                 "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
-                "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0)",
+                "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
                 "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0).",
                 "symfony/cache": "Required to PSR-6 cache bridge (^4.3.4).",
                 "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.2).",
@@ -1320,7 +1321,7 @@
                 "framework",
                 "laravel"
             ],
-            "time": "2019-12-03T15:23:55+00:00"
+            "time": "2019-12-19T18:16:22+00:00"
         },
         {
             "name": "laravel/passport",
@@ -1563,16 +1564,16 @@
         },
         {
             "name": "league/flysystem",
-            "version": "1.0.57",
+            "version": "1.0.61",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem.git",
-                "reference": "0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a"
+                "reference": "4fb13c01784a6c9f165a351e996871488ca2d8c9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a",
-                "reference": "0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/4fb13c01784a6c9f165a351e996871488ca2d8c9",
+                "reference": "4fb13c01784a6c9f165a351e996871488ca2d8c9",
                 "shasum": ""
             },
             "require": {
@@ -1643,7 +1644,7 @@
                 "sftp",
                 "storage"
             ],
-            "time": "2019-10-16T21:01:05+00:00"
+            "time": "2019-12-08T21:46:50+00:00"
         },
         {
             "name": "league/oauth2-server",
@@ -1724,16 +1725,16 @@
         },
         {
             "name": "mews/captcha",
-            "version": "3.0.1",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/mewebstudio/captcha.git",
-                "reference": "bef0db3663f1fe442ae803fb207dbe2bbfa10890"
+                "reference": "0fa11e098549ceb8f99f93dcf24719622d8c8078"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mewebstudio/captcha/zipball/bef0db3663f1fe442ae803fb207dbe2bbfa10890",
-                "reference": "bef0db3663f1fe442ae803fb207dbe2bbfa10890",
+                "url": "https://api.github.com/repos/mewebstudio/captcha/zipball/0fa11e098549ceb8f99f93dcf24719622d8c8078",
+                "reference": "0fa11e098549ceb8f99f93dcf24719622d8c8078",
                 "shasum": ""
             },
             "require": {
@@ -1789,7 +1790,7 @@
                 "laravel6 Captcha",
                 "laravel6 Security"
             ],
-            "time": "2019-09-05T22:33:04+00:00"
+            "time": "2019-12-05T11:15:51+00:00"
         },
         {
             "name": "monolog/monolog",
@@ -1874,16 +1875,16 @@
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.27.0",
+            "version": "2.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "13b8485a8690f103bf19cba64879c218b102b726"
+                "reference": "e2bcbcd43e67ee6101d321d5de916251d2870ca8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/13b8485a8690f103bf19cba64879c218b102b726",
-                "reference": "13b8485a8690f103bf19cba64879c218b102b726",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e2bcbcd43e67ee6101d321d5de916251d2870ca8",
+                "reference": "e2bcbcd43e67ee6101d321d5de916251d2870ca8",
                 "shasum": ""
             },
             "require": {
@@ -1940,7 +1941,7 @@
                 "datetime",
                 "time"
             ],
-            "time": "2019-11-20T06:59:06+00:00"
+            "time": "2019-12-16T16:30:25+00:00"
         },
         {
             "name": "nikic/php-parser",
@@ -2246,28 +2247,29 @@
         },
         {
             "name": "phpoption/phpoption",
-            "version": "1.6.0",
+            "version": "1.7.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/schmittjoh/php-option.git",
-                "reference": "f4e7a6a1382183412246f0d361078c29fb85089e"
+                "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/f4e7a6a1382183412246f0d361078c29fb85089e",
-                "reference": "f4e7a6a1382183412246f0d361078c29fb85089e",
+                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959",
+                "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.5.9 || ^7.0"
             },
             "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.3",
                 "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.6-dev"
+                    "dev-master": "1.7-dev"
                 }
             },
             "autoload": {
@@ -2296,7 +2298,7 @@
                 "php",
                 "type"
             ],
-            "time": "2019-11-30T20:20:49+00:00"
+            "time": "2019-12-15T19:35:24+00:00"
         },
         {
             "name": "phpseclib/phpseclib",
@@ -2873,20 +2875,20 @@
         },
         {
             "name": "psy/psysh",
-            "version": "v0.9.11",
+            "version": "v0.9.12",
             "source": {
                 "type": "git",
                 "url": "https://github.com/bobthecow/psysh.git",
-                "reference": "75d9ac1c16db676de27ab554a4152b594be4748e"
+                "reference": "90da7f37568aee36b116a030c5f99c915267edd4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/75d9ac1c16db676de27ab554a4152b594be4748e",
-                "reference": "75d9ac1c16db676de27ab554a4152b594be4748e",
+                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/90da7f37568aee36b116a030c5f99c915267edd4",
+                "reference": "90da7f37568aee36b116a030c5f99c915267edd4",
                 "shasum": ""
             },
             "require": {
-                "dnoegel/php-xdg-base-dir": "0.1",
+                "dnoegel/php-xdg-base-dir": "0.1.*",
                 "ext-json": "*",
                 "ext-tokenizer": "*",
                 "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*",
@@ -2943,7 +2945,7 @@
                 "interactive",
                 "shell"
             ],
-            "time": "2019-11-27T22:44:29+00:00"
+            "time": "2019-12-06T14:19:43+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
@@ -2987,22 +2989,22 @@
         },
         {
             "name": "ramsey/uuid",
-            "version": "3.9.1",
+            "version": "3.9.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ramsey/uuid.git",
-                "reference": "5ac2740e0c8c599d2bbe7f113a939f2b5b216c67"
+                "reference": "7779489a47d443f845271badbdcedfe4df8e06fb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/ramsey/uuid/zipball/5ac2740e0c8c599d2bbe7f113a939f2b5b216c67",
-                "reference": "5ac2740e0c8c599d2bbe7f113a939f2b5b216c67",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/7779489a47d443f845271badbdcedfe4df8e06fb",
+                "reference": "7779489a47d443f845271badbdcedfe4df8e06fb",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "paragonie/random_compat": "^1 | ^2 | 9.99.99",
-                "php": "^5.4 | ^7",
+                "php": "^5.4 | ^7 | ^8",
                 "symfony/polyfill-ctype": "^1.8"
             },
             "replace": {
@@ -3012,13 +3014,13 @@
                 "codeception/aspect-mock": "^1 | ^2",
                 "doctrine/annotations": "^1.2",
                 "goaop/framework": "1.0.0-alpha.2 | ^1 | ^2.1",
-                "jakub-onderka/php-parallel-lint": "^0.9.0",
-                "mockery/mockery": "^0.9.9",
+                "jakub-onderka/php-parallel-lint": "^1",
+                "mockery/mockery": "^0.9.11 | ^1",
                 "moontoast/math": "^1.1",
                 "paragonie/random-lib": "^2",
                 "php-mock/php-mock-phpunit": "^0.3 | ^1.1",
                 "phpunit/phpunit": "^4.8 | ^5.4 | ^6.5",
-                "squizlabs/php_codesniffer": "^2.3"
+                "squizlabs/php_codesniffer": "^3.5"
             },
             "suggest": {
                 "ext-ctype": "Provides support for PHP Ctype functions",
@@ -3070,7 +3072,7 @@
                 "identifier",
                 "uuid"
             ],
-            "time": "2019-12-01T04:55:27+00:00"
+            "time": "2019-12-17T08:18:51+00:00"
         },
         {
             "name": "swiftmailer/swiftmailer",
@@ -3136,16 +3138,16 @@
         },
         {
             "name": "symfony/console",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "f0aea3df20d15635b3cb9730ca5eea1c65b7f201"
+                "reference": "82437719dab1e6bdd28726af14cb345c2ec816d0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/f0aea3df20d15635b3cb9730ca5eea1c65b7f201",
-                "reference": "f0aea3df20d15635b3cb9730ca5eea1c65b7f201",
+                "url": "https://api.github.com/repos/symfony/console/zipball/82437719dab1e6bdd28726af14cb345c2ec816d0",
+                "reference": "82437719dab1e6bdd28726af14cb345c2ec816d0",
                 "shasum": ""
             },
             "require": {
@@ -3208,11 +3210,11 @@
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2019-12-01T10:06:17+00:00"
+            "time": "2019-12-17T10:32:23+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v5.0.1",
+            "version": "v5.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
@@ -3265,16 +3267,16 @@
         },
         {
             "name": "symfony/debug",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "b8600a1d7d20b0e80906398bb1f50612fa074a8e"
+                "reference": "5c4c1db977dc70bb3250e1308d3e8c6341aa38f5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/b8600a1d7d20b0e80906398bb1f50612fa074a8e",
-                "reference": "b8600a1d7d20b0e80906398bb1f50612fa074a8e",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/5c4c1db977dc70bb3250e1308d3e8c6341aa38f5",
+                "reference": "5c4c1db977dc70bb3250e1308d3e8c6341aa38f5",
                 "shasum": ""
             },
             "require": {
@@ -3317,20 +3319,20 @@
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2019-11-28T13:33:56+00:00"
+            "time": "2019-12-16T14:46:54+00:00"
         },
         {
             "name": "symfony/error-handler",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-                "reference": "a1ad02d62789efed1d2b2796f1c15e0c6a00fc3b"
+                "reference": "6d7d7712a6ff5215ec26215672293b154f1db8c1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/error-handler/zipball/a1ad02d62789efed1d2b2796f1c15e0c6a00fc3b",
-                "reference": "a1ad02d62789efed1d2b2796f1c15e0c6a00fc3b",
+                "url": "https://api.github.com/repos/symfony/error-handler/zipball/6d7d7712a6ff5215ec26215672293b154f1db8c1",
+                "reference": "6d7d7712a6ff5215ec26215672293b154f1db8c1",
                 "shasum": ""
             },
             "require": {
@@ -3373,11 +3375,11 @@
             ],
             "description": "Symfony ErrorHandler Component",
             "homepage": "https://symfony.com",
-            "time": "2019-12-01T08:46:01+00:00"
+            "time": "2019-12-16T14:46:54+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
@@ -3505,7 +3507,7 @@
         },
         {
             "name": "symfony/finder",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
@@ -3554,16 +3556,16 @@
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "8bccc59e61b41963d14c3dbdb23181e5c932a1d5"
+                "reference": "fcae1cff5b57b2a9c3aabefeb1527678705ddb62"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8bccc59e61b41963d14c3dbdb23181e5c932a1d5",
-                "reference": "8bccc59e61b41963d14c3dbdb23181e5c932a1d5",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/fcae1cff5b57b2a9c3aabefeb1527678705ddb62",
+                "reference": "fcae1cff5b57b2a9c3aabefeb1527678705ddb62",
                 "shasum": ""
             },
             "require": {
@@ -3605,20 +3607,20 @@
             ],
             "description": "Symfony HttpFoundation Component",
             "homepage": "https://symfony.com",
-            "time": "2019-11-28T13:33:56+00:00"
+            "time": "2019-12-19T15:57:49+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "e4187780ed26129ee86d5234afbebf085e144f88"
+                "reference": "fe310d2e95cd4c356836c8ecb0895a46d97fede2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/e4187780ed26129ee86d5234afbebf085e144f88",
-                "reference": "e4187780ed26129ee86d5234afbebf085e144f88",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fe310d2e95cd4c356836c8ecb0895a46d97fede2",
+                "reference": "fe310d2e95cd4c356836c8ecb0895a46d97fede2",
                 "shasum": ""
             },
             "require": {
@@ -3695,11 +3697,11 @@
             ],
             "description": "Symfony HttpKernel Component",
             "homepage": "https://symfony.com",
-            "time": "2019-12-01T14:06:38+00:00"
+            "time": "2019-12-19T16:23:40+00:00"
         },
         {
             "name": "symfony/mime",
-            "version": "v5.0.1",
+            "version": "v5.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
@@ -4220,16 +4222,16 @@
         },
         {
             "name": "symfony/process",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "51c0135ef3f44c5803b33dc60e96bf4f77752726"
+                "reference": "b84501ad50adb72a94fb460a5b5c91f693e99c9b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/51c0135ef3f44c5803b33dc60e96bf4f77752726",
-                "reference": "51c0135ef3f44c5803b33dc60e96bf4f77752726",
+                "url": "https://api.github.com/repos/symfony/process/zipball/b84501ad50adb72a94fb460a5b5c91f693e99c9b",
+                "reference": "b84501ad50adb72a94fb460a5b5c91f693e99c9b",
                 "shasum": ""
             },
             "require": {
@@ -4265,7 +4267,7 @@
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2019-11-28T13:33:56+00:00"
+            "time": "2019-12-06T10:06:46+00:00"
         },
         {
             "name": "symfony/psr-http-message-bridge",
@@ -4334,16 +4336,16 @@
         },
         {
             "name": "symfony/routing",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-                "reference": "51f3f20ad29329a0bdf5c0e2f722d3764b065273"
+                "reference": "628bcafae1b2043969378dcfbf9c196539a38722"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/routing/zipball/51f3f20ad29329a0bdf5c0e2f722d3764b065273",
-                "reference": "51f3f20ad29329a0bdf5c0e2f722d3764b065273",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/628bcafae1b2043969378dcfbf9c196539a38722",
+                "reference": "628bcafae1b2043969378dcfbf9c196539a38722",
                 "shasum": ""
             },
             "require": {
@@ -4406,7 +4408,7 @@
                 "uri",
                 "url"
             ],
-            "time": "2019-12-01T08:39:58+00:00"
+            "time": "2019-12-12T12:53:52+00:00"
         },
         {
             "name": "symfony/service-contracts",
@@ -4468,16 +4470,16 @@
         },
         {
             "name": "symfony/translation",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "897fb68ee7933372517b551d6f08c6d4bb0b8c40"
+                "reference": "f7669f48a9633bf8139bc026c755e894b7206677"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/897fb68ee7933372517b551d6f08c6d4bb0b8c40",
-                "reference": "897fb68ee7933372517b551d6f08c6d4bb0b8c40",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/f7669f48a9633bf8139bc026c755e894b7206677",
+                "reference": "f7669f48a9633bf8139bc026c755e894b7206677",
                 "shasum": ""
             },
             "require": {
@@ -4540,7 +4542,7 @@
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2019-11-12T17:18:47+00:00"
+            "time": "2019-12-12T12:53:52+00:00"
         },
         {
             "name": "symfony/translation-contracts",
@@ -4601,16 +4603,16 @@
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v4.4.1",
+            "version": "v4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "0a89a1dbbedd9fb2cfb2336556dec8305273c19a"
+                "reference": "be330f919bdb395d1e0c3f2bfb8948512d6bdd99"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0a89a1dbbedd9fb2cfb2336556dec8305273c19a",
-                "reference": "0a89a1dbbedd9fb2cfb2336556dec8305273c19a",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/be330f919bdb395d1e0c3f2bfb8948512d6bdd99",
+                "reference": "be330f919bdb395d1e0c3f2bfb8948512d6bdd99",
                 "shasum": ""
             },
             "require": {
@@ -4673,7 +4675,7 @@
                 "debug",
                 "dump"
             ],
-            "time": "2019-11-28T13:33:56+00:00"
+            "time": "2019-12-18T13:41:29+00:00"
         },
         {
             "name": "tijsverkoyen/css-to-inline-styles",
@@ -5143,16 +5145,16 @@
         },
         {
             "name": "facade/flare-client-php",
-            "version": "1.3.0",
+            "version": "1.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/facade/flare-client-php.git",
-                "reference": "0fd0c0a5c75a5acf04578311a08a7832e06a981c"
+                "reference": "24444ea0e1556f0a4b5fc8e61802caf72ae9a408"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/facade/flare-client-php/zipball/0fd0c0a5c75a5acf04578311a08a7832e06a981c",
-                "reference": "0fd0c0a5c75a5acf04578311a08a7832e06a981c",
+                "url": "https://api.github.com/repos/facade/flare-client-php/zipball/24444ea0e1556f0a4b5fc8e61802caf72ae9a408",
+                "reference": "24444ea0e1556f0a4b5fc8e61802caf72ae9a408",
                 "shasum": ""
             },
             "require": {
@@ -5160,7 +5162,7 @@
                 "illuminate/pipeline": "~5.5|~5.6|~5.7|~5.8|^6.0",
                 "php": "^7.1",
                 "symfony/http-foundation": "~3.3|~4.1",
-                "symfony/var-dumper": "^3.4|^4.0"
+                "symfony/var-dumper": "^3.4|^4.0|^5.0"
             },
             "require-dev": {
                 "larapack/dd": "^1.1",
@@ -5193,7 +5195,7 @@
                 "flare",
                 "reporting"
             ],
-            "time": "2019-11-27T10:09:46+00:00"
+            "time": "2019-12-15T18:28:38+00:00"
         },
         {
             "name": "facade/ignition",
@@ -5462,16 +5464,16 @@
         },
         {
             "name": "fzaninotto/faker",
-            "version": "v1.9.0",
+            "version": "v1.9.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/fzaninotto/Faker.git",
-                "reference": "27a216cbe72327b2d6369fab721a5843be71e57d"
+                "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/27a216cbe72327b2d6369fab721a5843be71e57d",
-                "reference": "27a216cbe72327b2d6369fab721a5843be71e57d",
+                "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/fc10d778e4b84d5bd315dad194661e091d307c6f",
+                "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f",
                 "shasum": ""
             },
             "require": {
@@ -5484,7 +5486,9 @@
             },
             "type": "library",
             "extra": {
-                "branch-alias": []
+                "branch-alias": {
+                    "dev-master": "1.9-dev"
+                }
             },
             "autoload": {
                 "psr-4": {
@@ -5506,7 +5510,7 @@
                 "faker",
                 "fixtures"
             ],
-            "time": "2019-11-14T13:13:06+00:00"
+            "time": "2019-12-12T13:22:17+00:00"
         },
         {
             "name": "hamcrest/hamcrest-php",
@@ -5624,16 +5628,16 @@
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.9.3",
+            "version": "1.9.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
+                "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
-                "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/579bb7356d91f9456ccd505f24ca8b667966a0a7",
+                "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7",
                 "shasum": ""
             },
             "require": {
@@ -5668,7 +5672,7 @@
                 "object",
                 "object graph"
             ],
-            "time": "2019-08-09T12:45:53+00:00"
+            "time": "2019-12-15T19:12:40+00:00"
         },
         {
             "name": "nunomaduro/collision",
@@ -6039,33 +6043,33 @@
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.9.0",
+            "version": "1.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203"
+                "reference": "d638ebbb58daba25a6a0dc7969e1358a0e3c6682"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203",
-                "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d638ebbb58daba25a6a0dc7969e1358a0e3c6682",
+                "reference": "d638ebbb58daba25a6a0dc7969e1358a0e3c6682",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.0.2",
                 "php": "^5.3|^7.0",
                 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
-                "sebastian/comparator": "^1.1|^2.0|^3.0",
+                "sebastian/comparator": "^1.2.3|^2.0|^3.0",
                 "sebastian/recursion-context": "^1.0|^2.0|^3.0"
             },
             "require-dev": {
-                "phpspec/phpspec": "^2.5|^3.2",
+                "phpspec/phpspec": "^2.5 || ^3.2",
                 "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.8.x-dev"
+                    "dev-master": "1.10.x-dev"
                 }
             },
             "autoload": {
@@ -6098,7 +6102,7 @@
                 "spy",
                 "stub"
             ],
-            "time": "2019-10-03T11:07:50+00:00"
+            "time": "2019-12-17T16:54:23+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
@@ -6354,16 +6358,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "8.4.3",
+            "version": "8.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "67f9e35bffc0dd52d55d565ddbe4230454fd6a4e"
+                "reference": "3ee1c1fd6fc264480c25b6fb8285edefe1702dab"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/67f9e35bffc0dd52d55d565ddbe4230454fd6a4e",
-                "reference": "67f9e35bffc0dd52d55d565ddbe4230454fd6a4e",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3ee1c1fd6fc264480c25b6fb8285edefe1702dab",
+                "reference": "3ee1c1fd6fc264480c25b6fb8285edefe1702dab",
                 "shasum": ""
             },
             "require": {
@@ -6407,7 +6411,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "8.4-dev"
+                    "dev-master": "8.5-dev"
                 }
             },
             "autoload": {
@@ -6433,20 +6437,20 @@
                 "testing",
                 "xunit"
             ],
-            "time": "2019-11-06T09:42:23+00:00"
+            "time": "2019-12-06T05:41:38+00:00"
         },
         {
             "name": "scrivo/highlight.php",
-            "version": "v9.15.10.0",
+            "version": "v9.17.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/scrivo/highlight.php.git",
-                "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e"
+                "reference": "5451a9ad6d638559cf2a092880f935c39776134e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/9ad3adb4456dc91196327498dbbce6aa1ba1239e",
-                "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e",
+                "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/5451a9ad6d638559cf2a092880f935c39776134e",
+                "reference": "5451a9ad6d638559cf2a092880f935c39776134e",
                 "shasum": ""
             },
             "require": {
@@ -6456,7 +6460,8 @@
             },
             "require-dev": {
                 "phpunit/phpunit": "^4.8|^5.7",
-                "symfony/finder": "^2.8"
+                "symfony/finder": "^3.4",
+                "symfony/var-dumper": "^3.4"
             },
             "suggest": {
                 "ext-dom": "Needed to make use of the features in the utilities namespace"
@@ -6478,18 +6483,18 @@
             "authors": [
                 {
                     "name": "Geert Bergman",
-                    "role": "Project Author",
-                    "homepage": "http://www.scrivo.org/"
+                    "homepage": "http://www.scrivo.org/",
+                    "role": "Project Author"
                 },
                 {
                     "name": "Vladimir Jimenez",
-                    "role": "Contributor",
-                    "homepage": "https://allejo.io"
+                    "homepage": "https://allejo.io",
+                    "role": "Maintainer"
                 },
                 {
                     "name": "Martin Folkers",
-                    "role": "Contributor",
-                    "homepage": "https://twobrain.io"
+                    "homepage": "https://twobrain.io",
+                    "role": "Contributor"
                 }
             ],
             "description": "Server side syntax highlighter that supports 185 languages. It's a PHP port of highlight.js",
@@ -6500,7 +6505,7 @@
                 "highlight.php",
                 "syntax"
             ],
-            "time": "2019-08-27T04:27:48+00:00"
+            "time": "2019-12-13T21:54:06+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
@@ -7119,7 +7124,7 @@
         },
         {
             "name": "symfony/filesystem",
-            "version": "v5.0.1",
+            "version": "v5.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
@@ -7169,7 +7174,7 @@
         },
         {
             "name": "symfony/options-resolver",
-            "version": "v5.0.1",
+            "version": "v5.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/options-resolver.git",
@@ -7282,7 +7287,7 @@
         },
         {
             "name": "symfony/stopwatch",
-            "version": "v5.0.1",
+            "version": "v5.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/stopwatch.git",

+ 23 - 0
config/anonaddy.php

@@ -157,6 +157,29 @@ return [
 
     'signing_key_fingerprint' => env('ANONADDY_SIGNING_KEY_FINGERPRINT'),
 
+    /*
+    |--------------------------------------------------------------------------
+    | DKIM Signing Key Path
+    |--------------------------------------------------------------------------
+    |
+    | This is the path to the private DKIM signing key to be used to sign emails for
+    | custom domains. The custom domains must have the correct selector records
+    |
+    */
+
+    'dkim_signing_key' => ! is_null(env('ANONADDY_DKIM_SIGNING_KEY')) ? file_get_contents(env('ANONADDY_DKIM_SIGNING_KEY')) : null,
+
+    /*
+    |--------------------------------------------------------------------------
+    | DKIM Signing Key Selector
+    |--------------------------------------------------------------------------
+    |
+    | This is the selector for the current DKIM signing key e.g. default
+    |
+    */
+
+    'dkim_selector' => env('ANONADDY_DKIM_SELECTOR'),
+
     /*
     |--------------------------------------------------------------------------
     | Username Blacklist

+ 1 - 1
config/mail.php

@@ -99,7 +99,7 @@ return [
     |
     */
 
-    'sendmail' => '/usr/sbin/sendmail -bs',
+    'sendmail' => '/usr/sbin/sendmail -t -XV',
 
     /*
     |--------------------------------------------------------------------------

+ 32 - 0
database/migrations/2019_12_09_151843_add_domain_sending_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 AddDomainSendingVerifiedAtToDomainsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('domains', function (Blueprint $table) {
+            $table->timestamp('domain_sending_verified_at')->nullable()->after('domain_verified_at');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('domains', function (Blueprint $table) {
+            $table->dropColumn('domain_sending_verified_at');
+        });
+    }
+}

+ 56 - 64
package-lock.json

@@ -1035,9 +1035,9 @@
             }
         },
         "acorn": {
-            "version": "6.3.0",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
-            "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA=="
+            "version": "6.4.0",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
+            "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw=="
         },
         "adjust-sourcemap-loader": {
             "version": "1.2.0",
@@ -1632,9 +1632,9 @@
             }
         },
         "buffer": {
-            "version": "4.9.1",
-            "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
-            "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
+            "version": "4.9.2",
+            "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+            "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
             "requires": {
                 "base64-js": "^1.0.2",
                 "ieee754": "^1.1.4",
@@ -1800,9 +1800,9 @@
             }
         },
         "chownr": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz",
-            "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A=="
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz",
+            "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw=="
         },
         "chrome-trace-event": {
             "version": "1.0.2",
@@ -2076,12 +2076,9 @@
             "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg=="
         },
         "console-browserify": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
-            "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
-            "requires": {
-                "date-now": "^0.1.4"
-            }
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+            "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="
         },
         "consolidate": {
             "version": "0.15.1",
@@ -2513,15 +2510,10 @@
             "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
             "dev": true
         },
-        "date-now": {
-            "version": "0.1.4",
-            "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
-            "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs="
-        },
         "dayjs": {
-            "version": "1.8.17",
-            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.17.tgz",
-            "integrity": "sha512-47VY/htqYqr9GHd7HW/h56PpQzRBSJcxIQFwqL3P20bMF/3az5c3PWdVY3LmPXFl6cQCYHL7c79b9ov+2bOBbw=="
+            "version": "1.8.18",
+            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.18.tgz",
+            "integrity": "sha512-JBMJZghNK8TtuoPnKNIzW9xavVVigld/zmZNpZSyQbkb2Opp55YIfZUpE4OEqPF/iyUVQTKcn1bC2HtC8B7s3g=="
         },
         "de-indent": {
             "version": "1.0.2",
@@ -2665,9 +2657,9 @@
             "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
         },
         "des.js": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
-            "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
+            "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
             "requires": {
                 "inherits": "^2.0.1",
                 "minimalistic-assert": "^1.0.0"
@@ -2815,9 +2807,9 @@
             "dev": true
         },
         "elliptic": {
-            "version": "6.5.1",
-            "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz",
-            "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==",
+            "version": "6.5.2",
+            "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
+            "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
             "requires": {
                 "bn.js": "^4.4.0",
                 "brorand": "^1.0.1",
@@ -6261,9 +6253,9 @@
             }
         },
         "parse-asn1": {
-            "version": "5.1.4",
-            "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
-            "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
+            "version": "5.1.5",
+            "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz",
+            "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==",
             "requires": {
                 "asn1.js": "^4.0.0",
                 "browserify-aes": "^1.0.0",
@@ -7945,9 +7937,9 @@
             }
         },
         "serialize-javascript": {
-            "version": "1.9.1",
-            "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz",
-            "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A=="
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
+            "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ=="
         },
         "serve-index": {
             "version": "1.9.1",
@@ -8522,9 +8514,9 @@
             }
         },
         "stream-shift": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
-            "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI="
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
+            "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
         },
         "string-argv": {
             "version": "0.0.2",
@@ -8765,15 +8757,15 @@
             }
         },
         "terser-webpack-plugin": {
-            "version": "1.4.1",
-            "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz",
-            "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==",
+            "version": "1.4.3",
+            "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
+            "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
             "requires": {
                 "cacache": "^12.0.2",
                 "find-cache-dir": "^2.1.0",
                 "is-wsl": "^1.1.0",
                 "schema-utils": "^1.0.0",
-                "serialize-javascript": "^1.7.0",
+                "serialize-javascript": "^2.1.2",
                 "source-map": "^0.6.1",
                 "terser": "^4.1.2",
                 "webpack-sources": "^1.4.0",
@@ -8781,9 +8773,9 @@
             },
             "dependencies": {
                 "commander": {
-                    "version": "2.20.0",
-                    "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
-                    "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
+                    "version": "2.20.3",
+                    "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+                    "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
                 },
                 "schema-utils": {
                     "version": "1.0.0",
@@ -8801,9 +8793,9 @@
                     "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
                 },
                 "terser": {
-                    "version": "4.2.1",
-                    "resolved": "https://registry.npmjs.org/terser/-/terser-4.2.1.tgz",
-                    "integrity": "sha512-cGbc5utAcX4a9+2GGVX4DsenG6v0x3glnDi5hx8816X1McEAwPlPgRtXPJzSBsbpILxZ8MQMT0KvArLuE0HP5A==",
+                    "version": "4.4.3",
+                    "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.3.tgz",
+                    "integrity": "sha512-0ikKraVtRDKGzHrzkCv5rUNDzqlhmhowOBqC0XqUHFpW+vJ45+20/IFBcebwKfiS2Z9fJin6Eo+F1zLZsxi8RA==",
                     "requires": {
                         "commander": "^2.20.0",
                         "source-map": "~0.6.1",
@@ -9214,14 +9206,14 @@
             "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw=="
         },
         "vm-browserify": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz",
-            "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw=="
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+            "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
         },
         "vue": {
-            "version": "2.6.10",
-            "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
-            "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
+            "version": "2.6.11",
+            "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
+            "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
         },
         "vue-good-table": {
             "version": "2.18.1",
@@ -9281,9 +9273,9 @@
             }
         },
         "vue-template-compiler": {
-            "version": "2.6.10",
-            "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz",
-            "integrity": "sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==",
+            "version": "2.6.11",
+            "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz",
+            "integrity": "sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==",
             "requires": {
                 "de-indent": "^1.0.2",
                 "he": "^1.1.0"
@@ -9313,9 +9305,9 @@
             }
         },
         "webpack": {
-            "version": "4.39.3",
-            "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.39.3.tgz",
-            "integrity": "sha512-BXSI9M211JyCVc3JxHWDpze85CvjC842EvpRsVTc/d15YJGlox7GIDd38kJgWrb3ZluyvIjgenbLDMBQPDcxYQ==",
+            "version": "4.41.3",
+            "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.3.tgz",
+            "integrity": "sha512-EcNzP9jGoxpQAXq1VOoTet0ik7/VVU1MovIfcUSAjLowc7GhcQku/sOXALvq5nPpSei2HF6VRhibeJSC3i/Law==",
             "requires": {
                 "@webassemblyjs/ast": "1.8.5",
                 "@webassemblyjs/helper-module-context": "1.8.5",
@@ -9337,7 +9329,7 @@
                 "node-libs-browser": "^2.2.1",
                 "schema-utils": "^1.0.0",
                 "tapable": "^1.1.3",
-                "terser-webpack-plugin": "^1.4.1",
+                "terser-webpack-plugin": "^1.4.3",
                 "watchpack": "^1.6.0",
                 "webpack-sources": "^1.4.1"
             },
@@ -9663,9 +9655,9 @@
             "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
         },
         "yallist": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
-            "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+            "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
         },
         "yargs": {
             "version": "12.0.5",

+ 3 - 3
package.json

@@ -13,7 +13,7 @@
     "dependencies": {
         "axios": "^0.18.1",
         "cross-env": "^5.2.1",
-        "dayjs": "^1.8.17",
+        "dayjs": "^1.8.18",
         "laravel-mix": "^4.1.4",
         "laravel-mix-purgecss": "^4.2.0",
         "lodash": "^4.17.15",
@@ -24,11 +24,11 @@
         "tailwindcss": "^1.1.4",
         "tippy.js": "^4.3.5",
         "v-clipboard": "^2.2.2",
-        "vue": "^2.6.10",
+        "vue": "^2.6.11",
         "vue-good-table": "^2.18.1",
         "vue-multiselect": "^2.1.6",
         "vue-notification": "^1.3.20",
-        "vue-template-compiler": "^2.6.10"
+        "vue-template-compiler": "^2.6.11"
     },
     "devDependencies": {
         "husky": "^2.7.0",

+ 128 - 37
resources/js/pages/Domains.vue

@@ -141,22 +141,20 @@
             @off="deactivateDomain(props.row.id)"
           />
         </span>
-        <span v-else-if="props.column.field === 'domain_verified_at'">
+        <span v-else-if="props.column.field === 'domain_sending_verified_at'">
           <span
             name="check"
-            v-if="props.row.domain_verified_at"
+            v-if="props.row.domain_sending_verified_at"
             class="py-1 px-2 bg-green-200 text-green-900 rounded-full text-sm"
           >
             verified
           </span>
           <button
             v-else
-            @click="recheckRecords(rows[props.row.originalIndex])"
+            @click="openCheckRecordsModal(rows[props.row.originalIndex])"
             class="focus:outline-none text-sm"
-            :class="recheckRecordsLoading ? 'cursor-not-allowed' : ''"
-            :disabled="recheckRecordsLoading"
           >
-            Recheck domain
+            Check Records
           </button>
         </span>
         <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
@@ -176,41 +174,48 @@
         </h1>
         <div class="mx-auto mb-6 w-24 border-b-2 border-grey-200"></div>
         <p class="mb-4">
-          To get started all you have to do is add an MX record to your domain and then add the
-          domain here by clicking the button above.
+          To get started all you have to do is add a TXT record to your domain to verify ownership
+          and then add the domain here by clicking the button above.
         </p>
         <p class="mb-4">
-          The new record needs to have the following values:
+          The TXT record needs to have the following values:
         </p>
         <p class="mb-4">
+          Type: <b>TXT</b><br />
           Host: <b>@</b><br />
-          Value: <b>{{ hostname }}</b
+          Value: <b>{{ aaVerify }}</b
           ><br />
-          Priority: <b>10</b><br />
-          TTL: <b>3600</b>
         </p>
         <p>
-          Once the DNS changes propagate you will be able to recieve emails at your own domain.
+          Once the DNS changes propagate and you have verified ownership of the domain you will need
+          to add a few more records to be able to recieve emails at your own domain.
         </p>
       </div>
     </div>
 
-    <Modal :open="addDomainModalOpen" @close="addDomainModalOpen = false">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
+    <Modal :open="addDomainModalOpen" @close="closeCheckRecordsModal">
+      <div v-if="!domainToCheck" class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6">
         <h2
           class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
         >
           Add new domain
         </h2>
-        <p class="mt-4 text-grey-700">
-          Make sure you add the following MX record to your domain first.<br /><br />
-          Host: <b>@</b><br />
-          Value: <b>{{ hostname }}</b
-          ><br />
-          Priority: <b>10</b><br />
-          TTL: <b>3600</b><br /><br />
-          Just include the domain/subdomain e.g. example.com without any http protocol.
+        <p class="mt-4 mb-2 text-grey-700">
+          To verify ownership of the domain, please add the following TXT record and then click Add
+          Domain below.
         </p>
+        <div class="table w-full">
+          <div class="table-row">
+            <div class="table-cell py-2 font-semibold">Type</div>
+            <div class="table-cell p-2 font-semibold">Host</div>
+            <div class="table-cell py-2 font-semibold">Value/Points to</div>
+          </div>
+          <div class="table-row">
+            <div class="table-cell py-2">TXT</div>
+            <div class="table-cell p-2">@</div>
+            <div class="table-cell py-2 break-all">aa-verify={{ aaVerify }}</div>
+          </div>
+        </div>
         <div class="mt-6">
           <p v-show="errors.newDomain" class="mb-3 text-red-500 text-sm">
             {{ errors.newDomain }}
@@ -240,6 +245,66 @@
           </button>
         </div>
       </div>
+      <div v-else class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6">
+        <h2
+          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
+        >
+          Check DNS records
+        </h2>
+        <p class="mt-4 mb-2 text-grey-700">
+          Please set the following DNS records for your custom domain. If you have more than one MX
+          record then the MX record below should have the lowest priority (e.g. 10).
+        </p>
+        <div class="table w-full">
+          <div class="table-row">
+            <div class="table-cell py-2 font-semibold">Type</div>
+            <div class="table-cell py-2 px-4 font-semibold">Host</div>
+            <div class="table-cell py-2 font-semibold">Value/Points to</div>
+          </div>
+          <div class="table-row">
+            <div class="table-cell py-2">MX</div>
+            <div class="table-cell py-2 px-4">@</div>
+            <div class="table-cell py-2 break-words">{{ hostname }}</div>
+          </div>
+          <div class="table-row">
+            <div class="table-cell py-2">TXT</div>
+            <div class="table-cell py-2 px-4">@</div>
+            <div class="table-cell py-2 break-words">v=spf1 include:spf.{{ domainName }} -all</div>
+          </div>
+          <div class="table-row">
+            <div class="table-cell py-2">CNAME</div>
+            <div class="table-cell py-2 px-4">dk1._domainkey</div>
+            <div class="table-cell py-2 break-words">dk1._domainkey.{{ domainName }}</div>
+          </div>
+          <div class="table-row">
+            <div class="table-cell py-2">CNAME</div>
+            <div class="table-cell py-2 px-4">dk2._domainkey</div>
+            <div class="table-cell py-2 break-words">dk2._domainkey.{{ domainName }}</div>
+          </div>
+          <div class="table-row">
+            <div class="table-cell py-2">TXT</div>
+            <div class="table-cell py-2 px-4">@</div>
+            <div class="table-cell py-2 break-words">v=DMARC1; p=quarantine; adkim=s</div>
+          </div>
+        </div>
+        <div class="mt-6">
+          <button
+            @click="checkRecords(domainToCheck)"
+            class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
+            :class="checkRecordsLoading ? 'cursor-not-allowed' : ''"
+            :disabled="checkRecordsLoading"
+          >
+            Check Records
+            <loader v-if="checkRecordsLoading" />
+          </button>
+          <button
+            @click="closeCheckRecordsModal"
+            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="domainDefaultRecipientModalOpen" @close="closeDomainDefaultRecipientModal">
@@ -336,6 +401,10 @@ export default {
       type: Array,
       required: true,
     },
+    domainName: {
+      type: String,
+      required: true,
+    },
     hostname: {
       type: String,
       required: true,
@@ -344,6 +413,10 @@ export default {
       type: Array,
       required: true,
     },
+    aaVerify: {
+      type: String,
+      required: true,
+    },
   },
   components: {
     Modal,
@@ -360,11 +433,12 @@ export default {
       addDomainLoading: false,
       addDomainModalOpen: false,
       domainIdToDelete: null,
-      domainIdToEdit: '',
+      domainIdToEdit: null,
       domainDescriptionToEdit: '',
+      domainToCheck: null,
       deleteDomainLoading: false,
       deleteDomainModalOpen: false,
-      recheckRecordsLoading: false,
+      checkRecordsLoading: false,
       domainDefaultRecipientModalOpen: false,
       defaultRecipientDomainToEdit: {},
       defaultRecipient: {},
@@ -403,8 +477,8 @@ export default {
           globalSearchDisabled: true,
         },
         {
-          label: 'Verified',
-          field: 'domain_verified_at',
+          label: 'Verified for Sending',
+          field: 'domain_sending_verified_at',
           globalSearchDisabled: true,
         },
         {
@@ -464,37 +538,46 @@ export default {
           this.addDomainLoading = false
           this.rows.push(data.data)
           this.newDomain = ''
-          this.addDomainModalOpen = false
+
+          this.domainToCheck = data.data
+
           this.success('Custom domain added')
         })
         .catch(error => {
           this.addDomainLoading = false
           if (error.response.status === 422) {
             this.error(error.response.data.errors.domain[0])
+          } else if (error.response.status === 429) {
+            this.error('Please wait a little while before checking the records again')
+          } else if (error.response.status === 404) {
+            this.warn(
+              'Verification TXT record not found, this could be due to DNS caching, please try again shortly.'
+            )
           } else {
             this.error()
           }
         })
     },
-    recheckRecords(domain) {
-      this.recheckRecordsLoading = true
+    checkRecords(domain) {
+      this.checkRecordsLoading = true
 
       axios
-        .get(`/domains/${domain.id}/recheck`)
+        .get(`/domains/${domain.id}/check-sending`)
         .then(({ data }) => {
-          this.recheckRecordsLoading = false
+          this.checkRecordsLoading = false
 
           if (data.success === true) {
+            this.closeCheckRecordsModal()
             this.success(data.message)
-            domain.domain_verified_at = data.data.domain_verified_at
+            domain.domain_sending_verified_at = data.data.domain_sending_verified_at
           } else {
             this.warn(data.message)
           }
         })
         .catch(error => {
-          this.recheckRecordsLoading = false
+          this.checkRecordsLoading = false
           if (error.response.status === 429) {
-            this.error('You can only recheck the records once per minute')
+            this.error('Please wait a little while before checking the records again')
           } else {
             this.error()
           }
@@ -518,6 +601,14 @@ export default {
       this.defaultRecipientDomainToEdit = {}
       this.defaultRecipient = {}
     },
+    openCheckRecordsModal(domain) {
+      this.domainToCheck = domain
+      this.addDomainModalOpen = true
+    },
+    closeCheckRecordsModal() {
+      this.domainToCheck = null
+      this.addDomainModalOpen = false
+    },
     editDomain(domain) {
       if (this.domainDescriptionToEdit.length > 100) {
         return this.error('Description cannot be more than 100 characters')
@@ -535,12 +626,12 @@ export default {
         )
         .then(response => {
           domain.description = this.domainDescriptionToEdit
-          this.domainIdToEdit = ''
+          this.domainIdToEdit = null
           this.domainDescriptionToEdit = ''
           this.success('Domain description updated')
         })
         .catch(error => {
-          this.domainIdToEdit = ''
+          this.domainIdToEdit = null
           this.domainDescriptionToEdit = ''
           this.error()
         })

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

@@ -4,6 +4,6 @@
     <div class="container py-8">
         @include('shared.status')
 
-        <domains :initial-domains="{{json_encode($domains)}}" hostname="{{config('anonaddy.hostname')}}" :recipient-options="{{ json_encode(Auth::user()->verifiedRecipients) }}" />
+        <domains :initial-domains="{{json_encode($domains)}}" domain-name="{{config('anonaddy.domain')}}" hostname="{{config('anonaddy.hostname')}}" :recipient-options="{{ json_encode(Auth::user()->verifiedRecipients) }}" aa-verify="{{ sha1(config('anonaddy.secret') . Auth::user()->id) }}" />
     </div>
 @endsection

+ 1 - 1
routes/web.php

@@ -25,7 +25,7 @@ Route::middleware(['auth', 'verified', '2fa'])->group(function () {
     Route::post('/recipients/email/resend', 'RecipientVerificationController@resend');
 
     Route::get('/domains', 'ShowDomainController@index')->name('domains.index');
-    Route::get('/domains/{id}/recheck', 'DomainVerificationController@recheck');
+    Route::get('/domains/{id}/check-sending', 'DomainVerificationController@checkSending');
 
     Route::get('/usernames', 'ShowAdditionalUsernameController@index')->name('usernames.index');
 

+ 50 - 1
tests/Feature/ReceiveEmailTest.php

@@ -775,7 +775,56 @@ class ReceiveEmailTest extends TestCase
 
         $domain = factory(Domain::class)->create([
             'user_id' => $this->user->id,
-            'domain' => 'example.com'
+            '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' => $domain->id,
+            'aliasable_type' => 'App\Domain',
+            'email' => 'ebay@example.com',
+            'local_part' => 'ebay',
+            'domain' => 'example.com',
+            'emails_forwarded' => 1,
+            'emails_blocked' => 0
+        ]);
+        $this->assertDatabaseHas('users', [
+            'id' => $this->user->id,
+            'username' => 'johndoe',
+            'bandwidth' => '871'
+        ]);
+        $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()
+    {
+        Mail::fake();
+
+        Mail::assertNothingSent();
+
+        $domain = factory(Domain::class)->create([
+            'user_id' => $this->user->id,
+            'domain' => 'example.com',
+            'domain_verified_at' => now(),
+            'domain_sending_verified_at' => now()
         ]);
 
         $this->artisan(

+ 5 - 5
tests/Feature/ShowDomainsTest.php

@@ -62,21 +62,21 @@ class DomainsTest extends TestCase
     }
 
     /** @test */
-    public function user_can_verify_domain_records()
+    public function user_can_verify_domain_sending_records()
     {
         $domain = factory(Domain::class)->create([
             'user_id' => $this->user->id,
-            'domain' => 'anonaddy.me'
+            'domain' => 'example.com'
         ]);
 
-        $response = $this->get('/domains/'.$domain->id.'/recheck');
+        $response = $this->get('/domains/'.$domain->id.'/check-sending');
 
         $response->assertStatus(200);
 
         $this->assertDatabaseHas('domains', [
             'user_id' => $this->user->id,
-            'domain' => 'anonaddy.me',
-            'domain_verified_at' => now()
+            'domain' => 'example.com',
+            'domain_sending_verified_at' => $response->json('data')['domain_sending_verified_at']
         ]);
     }
 }

+ 2 - 1
tests/TestCase.php

@@ -22,7 +22,8 @@ abstract class TestCase extends BaseTestCase
         config([
             'anonaddy.limit' => 1000,
             'anonaddy.domain' => 'anonaddy.com',
-            'anonaddy.all_domains' => ['anonaddy.com','anonaddy.me']
+            'anonaddy.all_domains' => ['anonaddy.com','anonaddy.me'],
+            'anonaddy.dkim_signing_key' => file_get_contents(base_path('tests/keys/TestDkimSigningKey'))
         ]);
 
         //$this->withoutExceptionHandling();

+ 27 - 0
tests/keys/TestDkimSigningKey

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpgIBAAKCAQEA+SAl1U5T0rclxKK0niDRaC7iszg3uIO0wQBWNVCB6F3O2Izp
+bly2VOy/9UvDeKF23xdyZA9GRDTEVZehK2HWDe+zeXJHHZ/GDgr6PjjTV6vOBfTZ
+SxO50f9h/AipT5T8hl18qo5DdYsMXqeTqKtNCXvqVTBsydgsPdy4R3kBi7dEnBUV
+/uljN9zY+MRF0YwhiBdDEBfHswMwjCKoWMQRexaExo+g4Z7TCTTPVpL5cW9xKISH
+IJ4gNxFHltVFs3JyamFYjQO8lJ6PN3U0zCgnowPsH0WS8z5YnG3EaEd9US4kRUQO
+jTEJ2B1Oa6iEKdRuLU0G1Ogt/8mD2ibppuUFEQIDAQABAoIBAQD2TRxaibhCCiTj
+BeIAvH2MvV4QxrIAUOayMj0JNPFa5PQOETLwk+UIjZEzWZaGe0xlQG8uEhPsdybt
+YJ5Nn8rbuHL+zUcWONK9K4lBXzlD4m4sYWk72uLKhe1AGugZ8DjCN0d78G8tlS3D
+n3ZTOhlOGR8S3m+QF5OWDdR7hV1L3RAuzPsYVE9srWJhZ5/v22xGqjM4ly5r6OoL
+6DuFXedodWjtHwwlAEDsZb5Ietzx87cFgcjO9IZxohGEu75gpKLmlUJY6Us+QK7u
+GEtOX17gzy/wbf6vDuHJ1MjrSYb+Py8/xzg36SYGJam6mkIg7ePoTQecnbm3yQwc
+dmXkDJRJAoGBAPrHoVvwYoNMBMahDnPh/Q6pHJ85hiRDwh3XXmQ0exCmMYv1FGT9
+ihZEm26um0/jn5pp5J/8Z1N6Sl1P2OY7htDlmcDjuK848kThfvOxsefI6PnvSojT
+QpbPz7epdxujVUYYUBwu/ZSgCbmGBvicMOC1I3+ECWtF/3hJMWGhS6rjAoGBAP5P
+s8vc02oDGq5ZF9KmJO0TV/BfnHG+Il2/cvMFkIlkV+6jcdNbCyUKLF4EF4iBNKPw
+JJ+B2NsMGE0EYAs/bN6Qf2V5qVqHzkerdNSTO56qvZjffdAMmgOmxckq3yIn9J0Z
+XrfXb3tCSX1E4aKVM3O3U6uMPYD/xiBPk4OSQI57AoGBAMwiYiu3skkUQTL6eQxF
+YbtkV0MZDSNJ8KJf5JsGyUJVNjGqDq7iX1we/rMK+KwAKrQEDLFaQTWHby8jxvgq
+bmRu8Ug9VeF0JmmBcNinxZxaQ6LncTGNXj/q6zeif60EuG41bkhyy8IGv8O/4L7Q
+TU0sxbeP4CbtNLZSRhsls7VnAoGBANjyXUt4YtJ5O3Wog24sViv46Hgi2G8f6GlZ
+PVKcWKUcePcXb68CaAt9FkRN8VV+A84mutdoCfsz4jcxmE2AGfv0APosQ5D7cboq
+RkxiHeZJedzEK6wkU+7xgqEOdbPJqh+q2Oa0XmIkNlUrQeFUrsP0jOg3hvBZQkem
+Gw2O4oupAoGBAPUXv53Ia58he5E5rQi8LpH7nYGL14WDRcd0whCrvpnleiNpgF25
+EgDJCteCqOcECnPKLawQhf1/x8akTarVVAw77OBCjWL2VVX6/cpj7f3gbE/vfgnE
+q38DITIEuJ2G+D5yAkWeb/d4G9qAmoKv0FMsSZ8ApN+86jn/imgu/Vxy
+-----END RSA PRIVATE KEY-----