Jelajahi Sumber

Upgrade to Laravel 9

Will Browning 3 tahun lalu
induk
melakukan
43e094ac93
92 mengubah file dengan 2146 tambahan dan 2262 penghapusan
  1. 4 3
      .husky/_/husky.sh
  2. 1 1
      .php-cs-fixer.php
  3. 1 1
      README.md
  4. 10 10
      SELF-HOSTING.md
  5. 3 3
      app/Console/Commands/CreateUser.php
  6. 1 1
      app/Console/Commands/ReceiveEmail.php
  7. 46 0
      app/Console/Commands/UpdateAppVersion.php
  8. 26 32
      app/CustomMailDriver/CustomMailManager.php
  9. 167 0
      app/CustomMailDriver/CustomMailer.php
  10. 0 157
      app/CustomMailDriver/CustomSendmailTransport.php
  11. 0 195
      app/CustomMailDriver/CustomSmtpTransport.php
  12. 40 0
      app/CustomMailDriver/Mime/Crypto/AlreadyEncrypted.php
  13. 302 0
      app/CustomMailDriver/Mime/Crypto/OpenPGPEncrypter.php
  14. 25 0
      app/CustomMailDriver/Mime/Encoder/RawContentEncoder.php
  15. 125 0
      app/CustomMailDriver/Mime/Part/EncryptedPart.php
  16. 48 0
      app/CustomMailDriver/Mime/Part/InlineImagePart.php
  17. 106 0
      app/CustomMailDriver/Mime/Part/TextPart.php
  18. 13 0
      app/Exceptions/CouldNotGetVersionException.php
  19. 0 74
      app/Helpers/AlreadyEncryptedSigner.php
  20. 71 0
      app/Helpers/GitVersionHelper.php
  21. 0 433
      app/Helpers/OpenPGPSigner.php
  22. 1 3
      app/Http/Controllers/AliasExportController.php
  23. 6 4
      app/Http/Controllers/Api/AppVersionController.php
  24. 4 4
      app/Http/Controllers/Auth/RegisterController.php
  25. 11 32
      app/Http/Controllers/Auth/WebauthnController.php
  26. 3 1
      app/Http/Controllers/Controller.php
  27. 3 0
      app/Http/Controllers/SettingController.php
  28. 2 1
      app/Http/Kernel.php
  29. 7 2
      app/Http/Middleware/TrustProxies.php
  30. 1 1
      app/Http/Requests/EditDefaultRecipientRequest.php
  31. 1 1
      app/Http/Requests/StoreAliasRecipientRequest.php
  32. 2 2
      app/Http/Requests/StoreAliasRequest.php
  33. 3 3
      app/Http/Requests/StoreDomainRequest.php
  34. 2 2
      app/Http/Requests/StoreRecipientRequest.php
  35. 1 1
      app/Http/Requests/StoreReorderRuleRequest.php
  36. 2 2
      app/Http/Requests/StoreUsernameRequest.php
  37. 4 1
      app/Jobs/DeleteAccount.php
  38. 31 67
      app/Mail/ForwardEmail.php
  39. 27 43
      app/Mail/ReplyToEmail.php
  40. 27 43
      app/Mail/SendFromEmail.php
  41. 13 3
      app/Mail/TokenExpiringSoon.php
  42. 4 1
      app/Models/Alias.php
  43. 2 1
      app/Models/DeletedUsername.php
  44. 3 1
      app/Models/Domain.php
  45. 2 1
      app/Models/EmailData.php
  46. 3 1
      app/Models/FailedDelivery.php
  47. 6 3
      app/Models/Recipient.php
  48. 2 1
      app/Models/Rule.php
  49. 7 3
      app/Models/User.php
  50. 3 1
      app/Models/Username.php
  51. 3 2
      app/Notifications/CustomVerifyEmail.php
  52. 6 26
      app/Notifications/DefaultRecipientUpdated.php
  53. 6 24
      app/Notifications/DisallowedReplySendAttempt.php
  54. 6 24
      app/Notifications/DomainMxRecordsInvalid.php
  55. 6 24
      app/Notifications/DomainUnverifiedForSending.php
  56. 5 26
      app/Notifications/FailedDeliveryNotification.php
  57. 7 2
      app/Notifications/GpgKeyExpired.php
  58. 6 24
      app/Notifications/NearBandwidthLimit.php
  59. 6 26
      app/Notifications/SpamReplySendAttempt.php
  60. 6 26
      app/Notifications/UsernameReminder.php
  61. 0 3
      app/Providers/AppServiceProvider.php
  62. 3 2
      app/Providers/CustomMailServiceProvider.php
  63. 10 0
      app/Providers/EventServiceProvider.php
  64. 1 0
      app/Providers/HelperServiceProvider.php
  65. 5 5
      app/Providers/RouteServiceProvider.php
  66. 3 23
      app/Services/Webauthn.php
  67. 2 5
      app/Traits/CheckUserRules.php
  68. 10 14
      composer.json
  69. 213 423
      composer.lock
  70. 25 38
      config/app.php
  71. 10 2
      config/broadcasting.php
  72. 3 3
      config/cache.php
  73. 13 1
      config/database.php
  74. 5 2
      config/filesystems.php
  75. 29 2
      config/logging.php
  76. 61 79
      config/mail.php
  77. 1 0
      config/services.php
  78. 0 60
      config/version.yml
  79. 69 12
      config/webauthn.php
  80. 31 0
      database/migrations/2022_06_29_103709_update_email_type_in_failed_deliveries_table.php
  81. 152 110
      package-lock.json
  82. 1 1
      package.json
  83. 10 9
      resources/js/components/WebauthnKeys.vue
  84. 10 0
      resources/js/webauthn.js
  85. 1 1
      resources/views/layouts/app.blade.php
  86. 4 4
      resources/views/settings/show.blade.php
  87. 25 5
      resources/views/vendor/webauthn/authenticate.blade.php
  88. 12 4
      resources/views/vendor/webauthn/register.blade.php
  89. 127 72
      routes/api.php
  90. 79 37
      routes/web.php
  91. 1 1
      tests/Feature/Api/AppVersionTest.php
  92. 1 1
      tests/Feature/Api/RulesTest.php

+ 4 - 3
.husky/_/husky.sh

@@ -1,7 +1,9 @@
 #!/bin/sh
 if [ -z "$husky_skip_init" ]; then
   debug () {
-    [ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1"
+    if [ "$HUSKY_DEBUG" = "1" ]; then
+      echo "husky (debug) - $1"
+    fi
   }
 
   readonly hook_name="$(basename "$0")"
@@ -23,8 +25,7 @@ if [ -z "$husky_skip_init" ]; then
 
   if [ $exitCode != 0 ]; then
     echo "husky - $hook_name hook exited with code $exitCode (error)"
-    exit $exitCode
   fi
 
-  exit 0
+  exit $exitCode
 fi

+ 1 - 1
.php-cs-fixer.php

@@ -13,7 +13,7 @@ $finder = Symfony\Component\Finder\Finder::create()
 $config = new PhpCsFixer\Config();
 
 return $config->setRules([
-        '@PSR2' => true,
+        '@PSR12' => true,
         'array_syntax' => ['syntax' => 'short'],
         'ordered_imports' => ['sort_algorithm' => 'alpha'],
         'no_unused_imports' => true,

+ 1 - 1
README.md

@@ -410,7 +410,7 @@ If you've forgotten your username you can request a reminder by entering your em
 
 Please use the backup code that you were shown when you enabled 2FA.
 
-5. Errors with U2F device
+5. Errors with hardware security key
 
 If you have a YubiKey and are using Windows and have an issue with your personal password/PIN you may need to reset the key using the YubiKey manager software.
 

+ 10 - 10
SELF-HOSTING.md

@@ -358,7 +358,7 @@ location = /robots.txt  { access_log off; log_not_found off; }
 error_page 404 /index.php;
 
 location ~ \.php$ {
-    fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
+    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
     fastcgi_index index.php;
     fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
     include fastcgi_params;
@@ -379,9 +379,9 @@ We won't restart nginx yet because it won't be able to find the SSL certificates
 
 ## Installing PHP
 
-We're going to install the latest version of PHP at the time of writing this - version 7.4
+We're going to install the latest version of PHP at the time of writing this - version 8.1
 
-First we need to add the following repository so we can install PHP8.0.
+First we need to add the following repository so we can install php8.1.
 
 ```bash
 sudo apt install software-properties-common
@@ -389,21 +389,21 @@ sudo add-apt-repository ppa:ondrej/php
 sudo apt update
 ```
 
-Install PHP8.0 and check the version.
+Install php8.1 and check the version.
 
 ```bash
-sudo apt install php8.0-fpm
-php-fpm8.0 -v
+sudo apt install php8.1-fpm
+php-fpm8.1 -v
 ```
 
 Install some required extensions:
 
 ```bash
-sudo apt install php8.0-common php8.0-mysql php8.0-dev php8.0-gmp php8.0-mbstring php8.0-dom php8.0-gd php8.0-imagick php8.0-opcache php8.0-soap php8.0-zip php8.0-cli php8.0-curl php8.0-mailparse php8.0-gnupg php8.0-redis -y
+sudo apt install php8.1-common php8.1-mysql php8.1-dev php8.1-gmp php8.1-mbstring php8.1-dom php8.1-gd php8.1-imagick php8.1-opcache php8.1-soap php8.1-zip php8.1-cli php8.1-curl php8.1-mailparse php8.1-gnupg php8.1-redis -y
 ```
 
 ```bash
-sudo nano /etc/php/8.0/fpm/pool.d/www.conf
+sudo nano /etc/php/8.1/fpm/pool.d/www.conf
 ```
 
 ```
@@ -413,10 +413,10 @@ listen.owner = johndoe
 listen.group = johndoe
 ```
 
-Restart php8.0-fpm to reflect the changes.
+Restart php8.1-fpm to reflect the changes.
 
 ```bash
-sudo service php8.0-fpm restart
+sudo service php8.1-fpm restart
 ```
 
 ## Let's Encrypt

+ 3 - 3
app/Console/Commands/CreateUser.php

@@ -55,14 +55,14 @@ class CreateUser extends Command
                 'regex:/^[a-zA-Z0-9]*$/',
                 'max:20',
                 'unique:usernames,username',
-                new NotDeletedUsername
+                new NotDeletedUsername()
             ],
             'email' => [
                 'required',
                 'email:rfc,dns',
                 'max:254',
-                new RegisterUniqueRecipient,
-                new NotLocalRecipient
+                new RegisterUniqueRecipient(),
+                new NotLocalRecipient()
             ],
         ]);
 

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

@@ -462,7 +462,7 @@ class ReceiveEmail extends Command
 
     protected function getParser($file)
     {
-        $parser = new Parser;
+        $parser = new Parser();
 
         // Fix some edge cases in from name e.g. "\" John Doe \"" <johndoe@example.com>
         $parser->addMiddleware(function ($mimePart, $next) {

+ 46 - 0
app/Console/Commands/UpdateAppVersion.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Helpers\GitVersionHelper;
+use Illuminate\Console\Command;
+
+class UpdateAppVersion extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'anonaddy:update-app-version';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Updates the cached app version';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $version = GitVersionHelper::cacheFreshVersion();
+        $this->info("AnonAddy version: {$version}");
+
+        return 0;
+    }
+}

+ 26 - 32
app/CustomMailDriver/CustomMailManager.php

@@ -3,51 +3,45 @@
 namespace App\CustomMailDriver;
 
 use Illuminate\Mail\MailManager;
+use InvalidArgumentException;
 
 class CustomMailManager extends MailManager
 {
     /**
-     * Create an instance of the Sendmail Swift Transport driver.
+     * Resolve the given mailer.
      *
-     * @param  array  $config
-     * @return \Swift_SendmailTransport
+     * @param string $name
+     * @return Mailer
      */
-    protected function createSendmailTransport(array $config)
+    protected function resolve($name): CustomMailer
     {
-        return new CustomSendmailTransport(
-            $config['path'] ?? $this->app['config']->get('mail.sendmail')
-        );
-    }
+        $config = $this->getConfig($name);
 
-    /**
-     * Create an instance of the SMTP Swift Transport driver.
-     *
-     * @param  array  $config
-     * @return \Swift_SmtpTransport
-     */
-    protected function createSmtpTransport(array $config)
-    {
-        // The Swift SMTP transport instance will allow us to use any SMTP backend
-        // for delivering mail such as Sendgrid, Amazon SES, or a custom server
-        // a developer has available. We will just pass this configured host.
-        $transport = new CustomSmtpTransport(
-            $config['host'],
-            $config['port']
+        if ($config === null) {
+            throw new InvalidArgumentException("Mailer [{$name}] is not defined.");
+        }
+
+        // Once we have created the mailer instance we will set a container instance
+        // on the mailer. This allows us to resolve mailer classes via containers
+        // for maximum testability on said classes instead of passing Closures.
+        $mailer = new CustomMailer(
+            $name,
+            $this->app['view'],
+            $this->createSymfonyTransport($config),
+            $this->app['events']
         );
 
-        if (! empty($config['encryption'])) {
-            $transport->setEncryption($config['encryption']);
+        if ($this->app->bound('queue')) {
+            $mailer->setQueue($this->app['queue']);
         }
 
-        // Once we have the transport we will check for the presence of a username
-        // and password. If we have it we will set the credentials on the Swift
-        // transporter instance so that we'll properly authenticate delivery.
-        if (isset($config['username'])) {
-            $transport->setUsername($config['username']);
-
-            $transport->setPassword($config['password']);
+        // Next we will set all of the global addresses on this mailer, which allows
+        // for easy unification of all "from" addresses as well as easy debugging
+        // of sent messages since these will be sent to a single email address.
+        foreach (['from', 'reply_to', 'to', 'return_path'] as $type) {
+            $this->setGlobalAddress($mailer, $config, $type);
         }
 
-        return $this->configureSmtpTransport($transport, $config);
+        return $mailer;
     }
 }

+ 167 - 0
app/CustomMailDriver/CustomMailer.php

@@ -0,0 +1,167 @@
+<?php
+
+namespace App\CustomMailDriver;
+
+use App\CustomMailDriver\Mime\Crypto\AlreadyEncrypted;
+use App\CustomMailDriver\Mime\Crypto\OpenPGPEncrypter;
+use App\Models\PostfixQueueId;
+use App\Models\Recipient;
+use App\Notifications\GpgKeyExpired;
+use Illuminate\Contracts\Mail\Mailable as MailableContract;
+use Illuminate\Database\QueryException;
+use Illuminate\Mail\Mailer;
+use Illuminate\Mail\SentMessage;
+use Illuminate\Support\Str;
+use Symfony\Component\Mailer\Envelope;
+use Symfony\Component\Mailer\Exception\RuntimeException;
+use Symfony\Component\Mime\Crypto\DkimOptions;
+use Symfony\Component\Mime\Crypto\DkimSigner;
+use Symfony\Component\Mime\Email;
+
+class CustomMailer extends Mailer
+{
+    /**
+     * Send a new message using a view.
+     *
+     * @param MailableContract|string|array  $view
+     * @param  array  $data
+     * @param  \Closure|string|null  $callback
+     * @return SentMessage|null
+     */
+    public function send($view, array $data = [], $callback = null)
+    {
+        if ($view instanceof MailableContract) {
+            return $this->sendMailable($view);
+        }
+
+        // First we need to parse the view, which could either be a string or an array
+        // containing both an HTML and plain text versions of the view which should
+        // be used when sending an e-mail. We will extract both of them out here.
+        [$view, $plain, $raw] = $this->parseView($view);
+
+        $data['message'] = $message = $this->createMessage();
+
+        // Once we have retrieved the view content for the e-mail we will set the body
+        // of this message using the HTML type, which will provide a simple wrapper
+        // to creating view based emails that are able to receive arrays of data.
+        if (! is_null($callback)) {
+            $callback($message);
+        }
+
+        $this->addContent($message, $view, $plain, $raw, $data);
+
+        // If a global "to" address has been set, we will set that address on the mail
+        // message. This is primarily useful during local development in which each
+        // message should be delivered into a single mail address for inspection.
+        if (isset($this->to['address'])) {
+            $this->setGlobalToAndRemoveCcAndBcc($message);
+        }
+
+        // Next we will determine if the message should be sent. We give the developer
+        // one final chance to stop this message and then we will send it to all of
+        // its recipients. We will then fire the sent event for the sent message.
+        $symfonyMessage = $message->getSymfonyMessage();
+
+        // OpenPGPEncrypter
+        if (isset($data['fingerprint']) && $data['fingerprint']) {
+            $recipient = Recipient::find($data['recipientId']);
+
+            try {
+                $encrypter = new OpenPGPEncrypter(config('anonaddy.signing_key_fingerprint'), $data['fingerprint'], "~/.gnupg");
+            } catch (RuntimeException $e) {
+                info($e->getMessage());
+                $encrypter = null;
+
+                $recipient->update(['should_encrypt' => false]);
+
+                $recipient->notify(new GpgKeyExpired());
+            }
+
+            if ($encrypter) {
+                $symfonyMessage = $encrypter->encrypt($symfonyMessage);
+            }
+        }
+
+        // Already encrypted
+        if (isset($data['encryptedParts']) && $data['encryptedParts']) {
+            $symfonyMessage = (new AlreadyEncrypted($data['encryptedParts']))->update($symfonyMessage);
+        }
+
+        // DkimSigner only for forwards, replies and sends...
+        if (isset($data['needsDkimSignature']) && $data['needsDkimSignature']) {
+            $dkimSigner = new DkimSigner(config('anonaddy.dkim_signing_key'), $data['aliasDomain'], config('anonaddy.dkim_selector'));
+
+            $options = (new DkimOptions())->headersToIgnore([
+                'List-Unsubscribe',
+                'Return-Path',
+                'Feedback-ID',
+                'Content-Type',
+                'Content-Description',
+                'Content-Disposition',
+                'Content-Transfer-Encoding',
+                'MIME-Version',
+                'Alias-To',
+                'X-AnonAddy-Authentication-Results',
+                'X-AnonAddy-Original-Sender',
+                'X-AnonAddy-Original-Envelope-From',
+                'X-AnonAddy-Original-From-Header',
+                'X-AnonAddy-Original-To',
+                'In-Reply-To',
+                'References',
+                'From',
+                'To',
+                'Message-ID',
+                'Subject',
+                'Date'
+            ])->toArray();
+            $signedEmail = $dkimSigner->sign($symfonyMessage, $options);
+            $symfonyMessage->setHeaders($signedEmail->getHeaders());
+        }
+
+        if ($this->shouldSendMessage($symfonyMessage, $data)) {
+            $symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage);
+
+            if ($symfonySentMessage) {
+                $sentMessage = new SentMessage($symfonySentMessage);
+
+                $this->dispatchSentEvent($sentMessage, $data);
+
+                try {
+                    // Get Postfix Queue ID and save in DB
+                    $id = str_replace("\r\n", "", Str::after($sentMessage->getDebug(), 'Ok: queued as '));
+
+                    PostfixQueueId::create([
+                        'queue_id' => $id
+                    ]);
+                } catch (QueryException $e) {
+                    // duplicate entry
+                    //Log::info('Failed to save Postfix Queue ID: ' . $id);
+                }
+
+                return $sentMessage;
+            }
+        }
+    }
+
+    /**
+     * Send a Symfony Email instance.
+     *
+     * @param  \Symfony\Component\Mime\Email  $message
+     * @return \Symfony\Component\Mailer\SentMessage|null
+     */
+    protected function sendSymfonyMessage(Email $message)
+    {
+        try {
+            $envelopeMessage = clone $message;
+            // This allows us to have the To: header set as the alias whilst still delivering to the correct RCPT TO.
+            if ($aliasTo = $message->getHeaders()->get('Alias-To')) {
+                $message->to($aliasTo->getValue());
+                $message->getHeaders()->remove('Alias-To');
+            }
+
+            return $this->transport->send($message, Envelope::create($envelopeMessage));
+        } finally {
+            //
+        }
+    }
+}

+ 0 - 157
app/CustomMailDriver/CustomSendmailTransport.php

@@ -1,157 +0,0 @@
-<?php
-
-namespace App\CustomMailDriver;
-
-use Swift_AddressEncoderException;
-use Swift_DependencyContainer;
-use Swift_Events_SendEvent;
-use Swift_Mime_SimpleMessage;
-use Swift_Transport_SendmailTransport;
-use Swift_TransportException;
-
-class CustomSendmailTransport extends Swift_Transport_SendmailTransport
-{
-    /**
-     * Create a new SendmailTransport, optionally using $command for sending.
-     *
-     * @param string $command
-     */
-    public function __construct($command = '/usr/sbin/sendmail -bs')
-    {
-        \call_user_func_array(
-            [$this, 'Swift_Transport_SendmailTransport::__construct'],
-            Swift_DependencyContainer::getInstance()
-                ->createDependenciesFor('transport.sendmail')
-        );
-
-        $this->setCommand($command);
-    }
-
-    /**
-     * Send the given Message.
-     *
-     * Recipient/sender data will be retrieved from the Message API.
-     * The return value is the number of recipients who were accepted for delivery.
-     *
-     * @param string[] $failedRecipients An array of failures by-reference
-     *
-     * @return int
-     */
-    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
-    {
-        if (!$this->isStarted()) {
-            $this->start();
-        }
-
-        $sent = 0;
-        $failedRecipients = (array) $failedRecipients;
-
-        if ($evt = $this->eventDispatcher->createSendEvent($this, $message)) {
-            $this->eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed');
-            if ($evt->bubbleCancelled()) {
-                return 0;
-            }
-        }
-
-        if (!$reversePath = $this->getReversePath($message)) {
-            $this->throwException(new Swift_TransportException('Cannot send message without a sender address'));
-        }
-
-        $to = (array) $message->getTo();
-        $cc = (array) $message->getCc();
-        $tos = array_merge($to, $cc);
-        $bcc = (array) $message->getBcc();
-
-        $message->setBcc([]);
-
-        // This allows us to have the To: header set as the alias whilst still delivering to the correct RCPT TO.
-        if ($aliasTo = $message->getHeaders()->get('Alias-To')) {
-            $message->setTo($aliasTo->getFieldBodyModel());
-            $message->getHeaders()->remove('Alias-To');
-        }
-
-        try {
-            $sent += $this->sendTo($message, $reversePath, $tos, $failedRecipients);
-            $sent += $this->sendBcc($message, $reversePath, $bcc, $failedRecipients);
-        } finally {
-            $message->setBcc($bcc);
-        }
-
-        if ($evt) {
-            if ($sent == \count($to) + \count($cc) + \count($bcc)) {
-                $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS);
-            } elseif ($sent > 0) {
-                $evt->setResult(Swift_Events_SendEvent::RESULT_TENTATIVE);
-            } else {
-                $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED);
-            }
-            $evt->setFailedRecipients($failedRecipients);
-            $this->eventDispatcher->dispatchEvent($evt, 'sendPerformed');
-        }
-
-        $message->generateId(); //Make sure a new Message ID is used
-
-        return $sent;
-    }
-
-    /** Send a message to the given To: recipients */
-    private function sendTo(Swift_Mime_SimpleMessage $message, $reversePath, array $to, array &$failedRecipients)
-    {
-        if (empty($to)) {
-            return 0;
-        }
-
-        return $this->doMailTransaction(
-            $message,
-            $reversePath,
-            array_keys($to),
-            $failedRecipients
-        );
-    }
-
-    /** Send a message to all Bcc: recipients */
-    private function sendBcc(Swift_Mime_SimpleMessage $message, $reversePath, array $bcc, array &$failedRecipients)
-    {
-        $sent = 0;
-        foreach ($bcc as $forwardPath => $name) {
-            $message->setBcc([$forwardPath => $name]);
-            $sent += $this->doMailTransaction(
-                $message,
-                $reversePath,
-                [$forwardPath],
-                $failedRecipients
-            );
-        }
-
-        return $sent;
-    }
-
-    /** Send an email to the given recipients from the given reverse path */
-    private function doMailTransaction($message, $reversePath, array $recipients, array &$failedRecipients)
-    {
-        $sent = 0;
-        $this->doMailFromCommand($reversePath);
-        foreach ($recipients as $forwardPath) {
-            try {
-                $this->doRcptToCommand($forwardPath);
-                ++$sent;
-            } catch (Swift_TransportException $e) {
-                $failedRecipients[] = $forwardPath;
-            } catch (Swift_AddressEncoderException $e) {
-                $failedRecipients[] = $forwardPath;
-            }
-        }
-
-        if (0 != $sent) {
-            $sent += \count($failedRecipients);
-            $this->doDataCommand($failedRecipients);
-            $sent -= \count($failedRecipients);
-
-            $this->streamMessage($message);
-        } else {
-            $this->reset();
-        }
-
-        return $sent;
-    }
-}

+ 0 - 195
app/CustomMailDriver/CustomSmtpTransport.php

@@ -1,195 +0,0 @@
-<?php
-
-namespace App\CustomMailDriver;
-
-use App\Models\PostfixQueueId;
-use Illuminate\Database\QueryException;
-use Illuminate\Support\Facades\Mail;
-use Illuminate\Support\Str;
-use Swift_AddressEncoderException;
-use Swift_DependencyContainer;
-use Swift_Events_SendEvent;
-use Swift_Mime_SimpleMessage;
-use Swift_Plugins_LoggerPlugin;
-use Swift_Plugins_Loggers_ArrayLogger;
-use Swift_Transport_EsmtpTransport;
-use Swift_TransportException;
-
-class CustomSmtpTransport extends Swift_Transport_EsmtpTransport
-{
-    /**
-     * @param string $host
-     * @param int    $port
-     * @param string|null $encryption SMTP encryption mode:
-     *        - null for plain SMTP (no encryption),
-     *        - 'tls' for SMTP with STARTTLS (best effort encryption),
-     *        - 'ssl' for SMTPS = SMTP over TLS (always encrypted).
-     */
-    public function __construct($host = 'localhost', $port = 25, $encryption = null)
-    {
-        \call_user_func_array(
-            [$this, 'Swift_Transport_EsmtpTransport::__construct'],
-            Swift_DependencyContainer::getInstance()
-                ->createDependenciesFor('transport.smtp')
-        );
-
-        $this->setHost($host);
-        $this->setPort($port);
-        $this->setEncryption($encryption);
-    }
-
-    /**
-     * Send the given Message.
-     *
-     * Recipient/sender data will be retrieved from the Message API.
-     * The return value is the number of recipients who were accepted for delivery.
-     *
-     * @param string[] $failedRecipients An array of failures by-reference
-     *
-     * @return int
-     */
-    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
-    {
-        if (!$this->isStarted()) {
-            $this->start();
-        }
-
-        $logger = new Swift_Plugins_Loggers_ArrayLogger();
-        Mail::getSwiftMailer()->registerPlugin(new Swift_Plugins_LoggerPlugin($logger));
-
-        $sent = 0;
-        $failedRecipients = (array) $failedRecipients;
-
-        if ($evt = $this->eventDispatcher->createSendEvent($this, $message)) {
-            $this->eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed');
-            if ($evt->bubbleCancelled()) {
-                return 0;
-            }
-        }
-
-        if (!$reversePath = $this->getReversePath($message)) {
-            $this->throwException(new Swift_TransportException('Cannot send message without a sender address'));
-        }
-
-        $to = (array) $message->getTo();
-        $cc = (array) $message->getCc();
-        $tos = array_merge($to, $cc);
-        $bcc = (array) $message->getBcc();
-
-        $message->setBcc([]);
-
-        // This allows us to have the To: header set as the alias whilst still delivering to the correct RCPT TO.
-        if ($aliasTo = $message->getHeaders()->get('Alias-To')) {
-            $message->setTo($aliasTo->getFieldBodyModel());
-            $message->getHeaders()->remove('Alias-To');
-        }
-
-        // Update Content IDs for inline image attachments
-        if ($oldCids = $message->getHeaders()->get('X-Old-Cids')) {
-            $oldCidsArray = explode(',', $oldCids->getFieldBodyModel());
-
-            $newCids = $message->getHeaders()->get('X-New-Cids');
-            $newCidsArray = explode(',', $newCids->getFieldBodyModel());
-
-            $message->getHeaders()->remove('X-Old-Cids');
-            $message->getHeaders()->remove('X-New-Cids');
-
-            $message->setBody(str_replace($oldCidsArray, $newCidsArray, $message->getBody()));
-        }
-
-        try {
-            $sent += $this->sendTo($message, $reversePath, $tos, $failedRecipients);
-            $sent += $this->sendBcc($message, $reversePath, $bcc, $failedRecipients);
-        } finally {
-            $message->setBcc($bcc);
-        }
-
-        if ($evt) {
-            if ($sent == \count($to) + \count($cc) + \count($bcc)) {
-                $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS);
-            } elseif ($sent > 0) {
-                $evt->setResult(Swift_Events_SendEvent::RESULT_TENTATIVE);
-            } else {
-                $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED);
-            }
-            $evt->setFailedRecipients($failedRecipients);
-            $this->eventDispatcher->dispatchEvent($evt, 'sendPerformed');
-        }
-
-        $message->generateId(); //Make sure a new Message ID is used
-
-        try {
-            // Get Postfix Queue ID and store in the database
-            $id = str_replace("\r\n", "", Str::after($logger->dump(), 'Ok: queued as '));
-
-            PostfixQueueId::create([
-                'queue_id' => $id
-            ]);
-        } catch (QueryException $e) {
-            // duplicate entry
-        }
-
-        return $sent;
-    }
-
-    /** Send a message to the given To: recipients */
-    private function sendTo(Swift_Mime_SimpleMessage $message, $reversePath, array $to, array &$failedRecipients)
-    {
-        if (empty($to)) {
-            return 0;
-        }
-
-        return $this->doMailTransaction(
-            $message,
-            $reversePath,
-            array_keys($to),
-            $failedRecipients
-        );
-    }
-
-    /** Send a message to all Bcc: recipients */
-    private function sendBcc(Swift_Mime_SimpleMessage $message, $reversePath, array $bcc, array &$failedRecipients)
-    {
-        $sent = 0;
-        foreach ($bcc as $forwardPath => $name) {
-            $message->setBcc([$forwardPath => $name]);
-            $sent += $this->doMailTransaction(
-                $message,
-                $reversePath,
-                [$forwardPath],
-                $failedRecipients
-            );
-        }
-
-        return $sent;
-    }
-
-    /** Send an email to the given recipients from the given reverse path */
-    private function doMailTransaction($message, $reversePath, array $recipients, array &$failedRecipients)
-    {
-        $sent = 0;
-        $this->doMailFromCommand($reversePath);
-        foreach ($recipients as $forwardPath) {
-            try {
-                $this->doRcptToCommand($forwardPath);
-                ++$sent;
-            } catch (Swift_TransportException $e) {
-                $failedRecipients[] = $forwardPath;
-            } catch (Swift_AddressEncoderException $e) {
-                $failedRecipients[] = $forwardPath;
-            }
-        }
-
-        if (0 != $sent) {
-            $sent += \count($failedRecipients);
-            $this->doDataCommand($failedRecipients);
-            $sent -= \count($failedRecipients);
-
-            $this->streamMessage($message);
-        } else {
-            $this->reset();
-        }
-
-        return $sent;
-    }
-}

+ 40 - 0
app/CustomMailDriver/Mime/Crypto/AlreadyEncrypted.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace App\CustomMailDriver\Mime\Crypto;
+
+use App\CustomMailDriver\Mime\Part\EncryptedPart;
+use Symfony\Component\Mime\Email;
+
+class AlreadyEncrypted
+{
+    protected $encryptedParts;
+
+    public function __construct($encryptedParts)
+    {
+        $this->encryptedParts = $encryptedParts;
+    }
+
+    public function update(Email $message): Email
+    {
+        $boundary = strtr(base64_encode(random_bytes(6)), '+/', '-_');
+
+        $headers = $message->getPreparedHeaders();
+
+        $headers->setHeaderBody('Parameterized', 'Content-Type', 'multipart/encrypted');
+        $headers->setHeaderParameter('Content-Type', 'protocol', 'application/pgp-encrypted');
+        $headers->setHeaderParameter('Content-Type', 'boundary', $boundary);
+
+        $message->setHeaders($headers);
+
+        $body = "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\r\n\r\n";
+
+        foreach ($this->encryptedParts as $part) {
+            $body .= "--{$boundary}\r\n";
+            $body .= $part->getMimePartStr()."\r\n";
+        }
+
+        $body .= "--{$boundary}--";
+
+        return $message->setBody(new EncryptedPart($body));
+    }
+}

+ 302 - 0
app/CustomMailDriver/Mime/Crypto/OpenPGPEncrypter.php

@@ -0,0 +1,302 @@
+<?php
+
+namespace App\CustomMailDriver\Mime\Crypto;
+
+use App\CustomMailDriver\Mime\Part\EncryptedPart;
+use Symfony\Component\Mailer\Exception\RuntimeException;
+use Symfony\Component\Mime\Email;
+
+class OpenPGPEncrypter
+{
+    protected $gnupg = null;
+
+    /**
+     * The signing hash algorithm. 'MD5', SHA1, or SHA256. SHA256 (the default) is highly recommended
+     * unless you need to deal with an old client that doesn't support it. SHA1 and MD5 are
+     * currently considered cryptographically weak.
+     *
+     * This is apparently not supported by the PHP GnuPG module.
+     *
+     * @type string
+     */
+    protected $micalg = 'SHA256';
+
+    protected $recipientKey = null;
+
+    /**
+     * The fingerprint of the key that will be used to sign the email. Populated either with
+     * autoAddSignature or addSignature.
+     *
+     * @type string
+     */
+    protected $signingKey;
+
+    /**
+     * An associative array of keyFingerprint=>passwords to decrypt secret keys (if needed).
+     * Populated by calling addKeyPassphrase. Pointless at the moment because the GnuPG module in
+     * PHP doesn't support decrypting keys with passwords. The command line client does, so this
+     * method stays for now.
+     *
+     * @type array
+     */
+    protected $keyPassphrases = [];
+
+    /**
+     * Specifies the home directory for the GnuPG keyrings. By default this is the user's home
+     * directory + /.gnupg, however when running on a web server (eg: Apache) the home directory
+     * will likely not exist and/or not be writable. Set this by calling setGPGHome before calling
+     * any other encryption/signing methods.
+     *
+     * @var string
+     */
+    protected $gnupgHome = null;
+
+
+    public function __construct($signingKey = null, $recipientKey = null, $gnupgHome = null)
+    {
+        $this->initGNUPG();
+        $this->signingKey    = $signingKey;
+        $this->recipientKey = $recipientKey;
+        $this->gnupgHome     = $gnupgHome;
+    }
+
+    /**
+     * @param string $micalg
+     */
+    public function setMicalg($micalg)
+    {
+        $this->micalg = $micalg;
+    }
+
+    /**
+     * @param $identifier
+     * @param null $passPhrase
+     *
+     * @throws RuntimeException
+     */
+    public function addSignature($identifier, $keyFingerprint = null, $passPhrase = null)
+    {
+        if (!$keyFingerprint) {
+            $keyFingerprint   = $this->getKey($identifier, 'sign');
+        }
+        $this->signingKey = $keyFingerprint;
+
+        if ($passPhrase) {
+            $this->addKeyPassphrase($keyFingerprint, $passPhrase);
+        }
+    }
+
+    /**
+     * @param $identifier
+     * @param $passPhrase
+     *
+     * @throws RuntimeException
+     */
+    public function addKeyPassphrase($identifier, $passPhrase)
+    {
+        $keyFingerprint                        = $this->getKey($identifier, 'sign');
+        $this->keyPassphrases[$keyFingerprint] = $passPhrase;
+    }
+
+    /**
+     * @param Email $email
+     *
+     * @return $this
+     *
+     * @throws RuntimeException
+     */
+    public function encrypt(Email $message): Email
+    {
+        $originalMessage = $message->toString();
+
+        $headers = $message->getPreparedHeaders();
+
+        $boundary = strtr(base64_encode(random_bytes(6)), '+/', '-_');
+
+        $headers->setHeaderBody('Parameterized', 'Content-Type', 'multipart/signed');
+        $headers->setHeaderParameter('Content-Type', 'micalg', sprintf("pgp-%s", strtolower($this->micalg)));
+        $headers->setHeaderParameter('Content-Type', 'protocol', 'application/pgp-signature');
+        $headers->setHeaderParameter('Content-Type', 'boundary', $boundary);
+
+        $message->setHeaders($headers);
+
+        if (!$this->signingKey) {
+            foreach ($message->getFrom() as $key => $value) {
+                $this->addSignature($this->getKey($key, 'sign'));
+            }
+        }
+
+        if (!$this->signingKey) {
+            throw new RuntimeException('Signing has been enabled, but no signature has been added. Use autoAddSignature() or addSignature()');
+        }
+
+        $lines = preg_split('/(\r\n|\r|\n)/', rtrim($originalMessage));
+
+        for ($i=0; $i<count($lines); $i++) {
+            $lines[$i] = rtrim($lines[$i])."\r\n";
+        }
+
+        // Remove excess trailing newlines (RFC3156 section 5.4)
+        $signedBody = rtrim(implode('', $lines))."\r\n";
+
+        $signature = $this->pgpSignString($signedBody, $this->signingKey);
+
+        // Fixes DKIM signature incorrect body hash for custom domains
+        $body = "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)\r\n\r\n";
+        $body .= "--{$boundary}\r\n";
+        $body .= $signedBody."\r\n";
+        $body .= "--{$boundary}\r\n";
+        $body .= "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n";
+        $body .= "Content-Description: OpenPGP digital signature\r\n";
+        $body .= "Content-Disposition: attachment; filename=\"signature.asc\"\r\n\r\n";
+        $body .= $signature."\r\n\r\n";
+        $body .= "--{$boundary}--";
+
+        $signed = sprintf("%s\r\n%s", $message->getHeaders()->get('content-type')->toString(), $body);
+
+        if (!$this->recipientKey) {
+            throw new RuntimeException('Encryption has been enabled, but no recipients have been added. Use autoAddRecipients() or addRecipient()');
+        }
+
+        //Create body from signed message
+        $encryptedBody = $this->pgpEncryptString($signed, $this->recipientKey);
+
+        $headers->setHeaderBody('Parameterized', 'Content-Type', 'multipart/encrypted');
+        $headers->setHeaderParameter('Content-Type', 'protocol', 'application/pgp-encrypted');
+        $headers->setHeaderParameter('Content-Type', 'boundary', $boundary);
+
+        // Fixes DKIM signature incorrect body hash for custom domains
+        $body = "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\r\n\r\n";
+        $body .= "--{$boundary}\r\n";
+        $body .= "Content-Type: application/pgp-encrypted\r\n";
+        $body .= "Content-Description: PGP/MIME version identification\r\n\r\n";
+        $body .= "Version: 1\r\n\r\n";
+        $body .= "--{$boundary}\r\n";
+        $body .= "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n";
+        $body .= "Content-Description: OpenPGP encrypted message\r\n";
+        $body .= "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n\r\n";
+        $body .= $encryptedBody."\r\n\r\n";
+        $body .= "--{$boundary}--";
+
+        return $message->setBody(new EncryptedPart($body));
+    }
+
+    /**
+     * @throws RuntimeException
+     */
+    protected function initGNUPG()
+    {
+        if (!class_exists('gnupg')) {
+            throw new RuntimeException('PHPMailerPGP requires the GnuPG class');
+        }
+
+        if (!$this->gnupgHome && isset($_SERVER['HOME'])) {
+            $this->gnupgHome = $_SERVER['HOME'] . '/.gnupg';
+        }
+
+        if (!$this->gnupgHome && getenv('HOME')) {
+            $this->gnupgHome = getenv('HOME') . '/.gnupg';
+        }
+
+        if (!$this->gnupg) {
+            $this->gnupg = new \gnupg();
+        }
+
+        $this->gnupg->seterrormode(\gnupg::ERROR_EXCEPTION);
+    }
+
+    /**
+     * @param $plaintext
+     * @param $keyFingerprint
+     *
+     * @return string
+     *
+     * @throws RuntimeException
+     */
+    protected function pgpSignString($plaintext, $keyFingerprint)
+    {
+        if (isset($this->keyPassphrases[$keyFingerprint]) && !$this->keyPassphrases[$keyFingerprint]) {
+            $passPhrase = $this->keyPassphrases[$keyFingerprint];
+        } else {
+            $passPhrase = null;
+        }
+
+        $this->gnupg->clearsignkeys();
+        $this->gnupg->addsignkey($keyFingerprint, $passPhrase);
+        $this->gnupg->setsignmode(\gnupg::SIG_MODE_DETACH);
+        $this->gnupg->setarmor(1);
+
+        $signed = $this->gnupg->sign($plaintext);
+
+        if ($signed) {
+            return $signed;
+        }
+
+        throw new RuntimeException('Unable to sign message (perhaps the secret key is encrypted with a passphrase?)');
+    }
+
+    /**
+     * @param $plaintext
+     * @param $keyFingerprints
+     *
+     * @return string
+     *
+     * @throws RuntimeException
+     */
+    protected function pgpEncryptString($plaintext, $keyFingerprint)
+    {
+        $this->gnupg->clearencryptkeys();
+
+        $this->gnupg->addencryptkey($keyFingerprint);
+
+        $this->gnupg->setarmor(1);
+
+        $encrypted = $this->gnupg->encrypt($plaintext);
+
+        if ($encrypted) {
+            return $encrypted;
+        }
+
+        throw new RuntimeException('Unable to encrypt message');
+    }
+
+    /**
+     * @param $identifier
+     * @param $purpose
+     *
+     * @return string
+     *
+     * @throws RuntimeException
+     */
+    protected function getKey($identifier, $purpose)
+    {
+        $keys         = $this->gnupg->keyinfo($identifier);
+        $fingerprints = [];
+
+        foreach ($keys as $key) {
+            if ($this->isValidKey($key, $purpose)) {
+                foreach ($key['subkeys'] as $subKey) {
+                    if ($this->isValidKey($subKey, $purpose)) {
+                        $fingerprints[] = $subKey['fingerprint'];
+                    }
+                }
+            }
+        }
+
+        // Return first available to encrypt
+        if (count($fingerprints) >= 1) {
+            return $fingerprints[0];
+        }
+
+        /* if (count($fingerprints) > 1) {
+            throw new Swift_SwiftException(sprintf('Found more than one active key for %s use addRecipient() or addSignature()', $identifier));
+        } */
+
+        throw new RuntimeException(sprintf('Unable to find an active key to %s for %s,try importing keys first', $purpose, $identifier));
+    }
+
+    protected function isValidKey($key, $purpose)
+    {
+        return !($key['disabled'] || $key['expired'] || $key['revoked'] || ($purpose == 'sign' && !$key['can_sign']) || ($purpose == 'encrypt' && !$key['can_encrypt']));
+    }
+}

+ 25 - 0
app/CustomMailDriver/Mime/Encoder/RawContentEncoder.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\CustomMailDriver\Mime\Encoder;
+
+use Symfony\Component\Mime\Encoder\ContentEncoderInterface;
+
+final class RawContentEncoder implements ContentEncoderInterface
+{
+    public function encodeByteStream($stream, int $maxLineLength = 0): iterable
+    {
+        while (!feof($stream)) {
+            yield fread($stream, 8192);
+        }
+    }
+
+    public function getName(): string
+    {
+        return 'raw';
+    }
+
+    public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
+    {
+        return $string;
+    }
+}

+ 125 - 0
app/CustomMailDriver/Mime/Part/EncryptedPart.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace App\CustomMailDriver\Mime\Part;
+
+use App\CustomMailDriver\Mime\Encoder\RawContentEncoder;
+use Symfony\Component\Mime\Encoder\ContentEncoderInterface;
+use Symfony\Component\Mime\Header\Headers;
+use Symfony\Component\Mime\Part\AbstractPart;
+
+class EncryptedPart extends AbstractPart
+{
+    /** @internal */
+    protected $_headers;
+
+    private $body;
+    private $charset;
+    private $subtype;
+    /**
+     * @var ?string
+     */
+    private $disposition;
+    private $seekable;
+
+    /**
+     * @param resource|string $body
+     */
+    public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain')
+    {
+        unset($this->_headers);
+
+        parent::__construct();
+
+        if (!\is_string($body) && !\is_resource($body)) {
+            throw new \TypeError(sprintf('The body of "%s" must be a string or a resource (got "%s").', self::class, get_debug_type($body)));
+        }
+
+        $this->body = $body;
+        $this->charset = $charset;
+        $this->subtype = $subtype;
+        $this->seekable = \is_resource($body) ? stream_get_meta_data($body)['seekable'] && 0 === fseek($body, 0, \SEEK_CUR) : null;
+    }
+
+    public function getMediaType(): string
+    {
+        return 'text';
+    }
+
+    public function getMediaSubtype(): string
+    {
+        return $this->subtype;
+    }
+
+    public function getBody(): string
+    {
+        if (null === $this->seekable) {
+            return $this->body;
+        }
+
+        if ($this->seekable) {
+            rewind($this->body);
+        }
+
+        return stream_get_contents($this->body) ?: '';
+    }
+
+    public function bodyToString(): string
+    {
+        return $this->getEncoder()->encodeString($this->getBody(), $this->charset);
+    }
+
+    public function bodyToIterable(): iterable
+    {
+        if (null !== $this->seekable) {
+            if ($this->seekable) {
+                rewind($this->body);
+            }
+            yield from $this->getEncoder()->encodeByteStream($this->body);
+        } else {
+            yield $this->getEncoder()->encodeString($this->body);
+        }
+    }
+
+    public function getPreparedHeaders(): Headers
+    {
+        return clone new Headers();
+    }
+
+    public function asDebugString(): string
+    {
+        $str = parent::asDebugString();
+        if (null !== $this->charset) {
+            $str .= ' charset: '.$this->charset;
+        }
+        if (null !== $this->disposition) {
+            $str .= ' disposition: '.$this->disposition;
+        }
+
+        return $str;
+    }
+
+    private function getEncoder(): ContentEncoderInterface
+    {
+        return new RawContentEncoder();
+    }
+
+    public function __sleep(): array
+    {
+        // convert resources to strings for serialization
+        if (null !== $this->seekable) {
+            $this->body = $this->getBody();
+        }
+
+        $this->_headers = $this->getHeaders();
+
+        return ['_headers', 'body', 'charset', 'subtype', 'disposition', 'name', 'encoding'];
+    }
+
+    public function __wakeup()
+    {
+        $r = new \ReflectionProperty(AbstractPart::class, 'headers');
+        $r->setAccessible(true);
+        $r->setValue($this, $this->_headers);
+        unset($this->_headers);
+    }
+}

+ 48 - 0
app/CustomMailDriver/Mime/Part/InlineImagePart.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\CustomMailDriver\Mime\Part;
+
+use Symfony\Component\Mime\Header\Headers;
+use Symfony\Component\Mime\Part\DataPart;
+
+class InlineImagePart extends DataPart
+{
+    /**
+     * Sets the content-id of the file.
+     *
+     * @return $this
+     */
+    public function setContentId(string $cid): static
+    {
+        $this->cid = $cid;
+
+        return $this;
+    }
+
+    /**
+     * Sets the name of the file.
+     *
+     * @return $this
+     */
+    public function setFileName(string $filename): static
+    {
+        $this->filename = $filename;
+
+        return $this;
+    }
+
+    public function getPreparedHeaders(): Headers
+    {
+        $headers = parent::getPreparedHeaders();
+
+        if (null !== $this->cid) {
+            $headers->setHeaderBody('Id', 'Content-ID', $this->cid);
+        }
+
+        if (null !== $this->filename) {
+            $headers->setHeaderParameter('Content-Disposition', 'filename', $this->filename);
+        }
+
+        return $headers;
+    }
+}

+ 106 - 0
app/CustomMailDriver/Mime/Part/TextPart.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace App\CustomMailDriver\Mime\Part;
+
+use Symfony\Component\Mime\Exception\InvalidArgumentException;
+use Symfony\Component\Mime\Header\Headers;
+use Symfony\Component\Mime\Part\AbstractPart;
+
+class TextPart extends AbstractPart
+{
+    /** @internal */
+    protected $_headers;
+
+    private static $encoders = [];
+
+    private $body;
+    private $boundary;
+    private $charset;
+    private $subtype;
+    /**
+     * @var ?string
+     */
+    private $disposition;
+    private $name;
+    private $encoding;
+    private $seekable;
+
+    /**
+     * @param resource|string $body
+     */
+    public function __construct($body, ?string $boundary, ?string $charset = 'utf-8', string $subtype = 'plain', string $encoding = null)
+    {
+        unset($this->_headers);
+
+        parent::__construct();
+
+        if (!\is_string($body) && !\is_resource($body)) {
+            throw new \TypeError(sprintf('The body of "%s" must be a string or a resource (got "%s").', self::class, get_debug_type($body)));
+        }
+
+        $this->body = $body;
+        $this->boundary = $boundary;
+        $this->charset = $charset;
+        $this->subtype = $subtype;
+        $this->seekable = \is_resource($body) ? stream_get_meta_data($body)['seekable'] && 0 === fseek($body, 0, \SEEK_CUR) : null;
+
+        if (null === $encoding) {
+            $this->encoding = $this->chooseEncoding();
+        } else {
+            if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) {
+                throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding));
+            }
+            $this->encoding = $encoding;
+        }
+    }
+
+    public function getMediaType(): string
+    {
+        return 'text';
+    }
+
+    public function getMediaSubtype(): string
+    {
+        return $this->subtype;
+    }
+
+    public function bodyToString(): string
+    {
+        return $this->getEncoder()->encodeString($this->getBody(), $this->charset);
+    }
+
+    public function bodyToIterable(): iterable
+    {
+        if (null !== $this->seekable) {
+            if ($this->seekable) {
+                rewind($this->body);
+            }
+            yield from $this->getEncoder()->encodeByteStream($this->body);
+        } else {
+            yield $this->getEncoder()->encodeString($this->body);
+        }
+    }
+
+    public function getPreparedHeaders(): Headers
+    {
+        $headers = parent::getPreparedHeaders();
+
+        $headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype());
+        if ($this->charset) {
+            $headers->setHeaderParameter('Content-Type', 'charset', $this->charset);
+        }
+        if ($this->name && 'form-data' !== $this->disposition) {
+            $headers->setHeaderParameter('Content-Type', 'name', $this->name);
+        }
+        $headers->setHeaderBody('Text', 'Content-Transfer-Encoding', $this->encoding);
+
+        if (!$headers->has('Content-Disposition') && null !== $this->disposition) {
+            $headers->setHeaderBody('Parameterized', 'Content-Disposition', $this->disposition);
+            if ($this->name) {
+                $headers->setHeaderParameter('Content-Disposition', 'name', $this->name);
+            }
+        }
+
+        return $headers;
+    }
+}

+ 13 - 0
app/Exceptions/CouldNotGetVersionException.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Exceptions;
+
+use RuntimeException;
+
+class CouldNotGetVersionException extends RuntimeException
+{
+    public function __construct()
+    {
+        parent::__construct("Could not get version string (`git describe` failed)");
+    }
+}

+ 0 - 74
app/Helpers/AlreadyEncryptedSigner.php

@@ -1,74 +0,0 @@
-<?php
-
-namespace App\Helpers;
-
-use Swift_DependencyContainer;
-use Swift_Message;
-use Swift_Signers_BodySigner;
-use Swift_SwiftException;
-
-class AlreadyEncryptedSigner implements Swift_Signers_BodySigner
-{
-    protected $attachments;
-
-    public function __construct($attachments)
-    {
-        $this->attachments = $attachments;
-    }
-
-    /**
-     * @param Swift_Message $message
-     *
-     * @return $this
-     *
-     * @throws Swift_DependencyException
-     * @throws Swift_SwiftException
-     */
-    public function signMessage(Swift_Message $message)
-    {
-        $message->setChildren([]);
-
-        $message->setEncoder(Swift_DependencyContainer::getInstance()->lookup('mime.rawcontentencoder'));
-
-        $type = $message->getHeaders()->get('Content-Type');
-
-        $type->setValue('multipart/encrypted');
-
-        $type->setParameters([
-            'protocol' => 'application/pgp-encrypted',
-            'boundary' => $message->getBoundary()
-        ]);
-
-        $body = 'This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)' . PHP_EOL;
-
-        foreach ($this->attachments as $attachment) {
-            $body .= '--' . $message->getBoundary() . PHP_EOL;
-            $body .= $attachment->getMimePartStr() . PHP_EOL;
-        }
-
-        $body .= '--'. $message->getBoundary() . '--';
-
-        $message->setBody($body);
-
-        $messageHeaders = $message->getHeaders();
-        $messageHeaders->removeAll('Content-Transfer-Encoding');
-
-        return $this;
-    }
-
-    /**
-     * @return array
-     */
-    public function getAlteredHeaders()
-    {
-        return ['Content-Type', 'Content-Transfer-Encoding', 'Content-Disposition', 'Content-Description'];
-    }
-
-    /**
-     * @return $this
-     */
-    public function reset()
-    {
-        return $this;
-    }
-}

+ 71 - 0
app/Helpers/GitVersionHelper.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Helpers;
+
+use App\Exceptions\CouldNotGetVersionException;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Str;
+use Symfony\Component\Process\Exception\RuntimeException;
+use Symfony\Component\Process\Process;
+
+class GitVersionHelper
+{
+    public static function version()
+    {
+        if (Cache::has('app-version')) {
+            return Cache::get('app-version');
+        }
+
+        return self::cacheFreshVersion();
+    }
+
+    public static function cacheFreshVersion()
+    {
+        $version = self::freshVersion();
+        Cache::put('app-version', $version);
+
+        return $version;
+    }
+
+    public static function freshVersion()
+    {
+        $path = base_path();
+
+        // Get version string from git
+        $command = 'git describe --tags $(git rev-list --tags --max-count=1)';
+        $fail = false;
+        if (class_exists('\Symfony\Component\Process\Process')) {
+            try {
+                if (method_exists(Process::class, 'fromShellCommandline')) {
+                    $process = Process::fromShellCommandline($command, $path);
+                } else {
+                    $process = new Process([$command], $path);
+                }
+
+                $process->mustRun();
+                $output = $process->getOutput();
+            } catch (RuntimeException $e) {
+                $fail = true;
+            }
+        } else {
+            // Remember current directory
+            $dir = getcwd();
+
+            // Change to base directory
+            chdir($path);
+
+            $output = shell_exec($command);
+
+            // Change back
+            chdir($dir);
+
+            $fail = $output === null;
+        }
+
+        if ($fail) {
+            throw new CouldNotGetVersionException();
+        }
+
+        return Str::of($output)->after('v')->trim();
+    }
+}

+ 0 - 433
app/Helpers/OpenPGPSigner.php

@@ -1,433 +0,0 @@
-<?php
-
-namespace App\Helpers;
-
-use Swift_DependencyContainer;
-use Swift_Message;
-use Swift_Signers_BodySigner;
-use Swift_SwiftException;
-
-/*
- * This file is part of SwiftMailer.
- * (c) 2004-2009 Chris Corbyn
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Message Signer used to apply OpenPGP Signature/Encryption to a message.
- *
- * @author Artem Zhuravlev <infzanoza@gmail.com>
- */
-class OpenPGPSigner implements Swift_Signers_BodySigner
-{
-    protected $gnupg = null;
-
-    /**
-     * The signing hash algorithm. 'MD5', SHA1, or SHA256. SHA256 (the default) is highly recommended
-     * unless you need to deal with an old client that doesn't support it. SHA1 and MD5 are
-     * currently considered cryptographically weak.
-     *
-     * This is apparently not supported by the PHP GnuPG module.
-     *
-     * @type string
-     */
-    protected $micalg = 'SHA256';
-
-    /**
-     * An associative array of identifier=>keyFingerprint for the recipients we'll encrypt the email
-     * to, where identifier is usually the email address, but could be anything used to look up a
-     * key (including the fingerprint itself). This is populated either by autoAddRecipients or by
-     * calling addRecipient.
-     *
-     * @type array
-     */
-    protected $recipientKeys = [];
-
-    /**
-     * The fingerprint of the key that will be used to sign the email. Populated either with
-     * autoAddSignature or addSignature.
-     *
-     * @type string
-     */
-    protected $signingKey;
-
-    /**
-     * An associative array of keyFingerprint=>passwords to decrypt secret keys (if needed).
-     * Populated by calling addKeyPassphrase. Pointless at the moment because the GnuPG module in
-     * PHP doesn't support decrypting keys with passwords. The command line client does, so this
-     * method stays for now.
-     *
-     * @type array
-     */
-    protected $keyPassphrases = [];
-
-    /**
-     * Specifies the home directory for the GnuPG keyrings. By default this is the user's home
-     * directory + /.gnupg, however when running on a web server (eg: Apache) the home directory
-     * will likely not exist and/or not be writable. Set this by calling setGPGHome before calling
-     * any other encryption/signing methods.
-     *
-     * @var string
-     */
-    protected $gnupgHome = null;
-
-    /**
-     * @var bool
-     */
-    protected $encrypt = true;
-
-
-    public function __construct($signingKey = null, $recipientKeys = [], $gnupgHome = null)
-    {
-        $this->initGNUPG();
-        $this->signingKey    = $signingKey;
-        $this->recipientKeys = $recipientKeys;
-        $this->gnupgHome     = $gnupgHome;
-    }
-
-    public static function newInstance($signingKey = null, $recipientKeys = [], $gnupgHome = null)
-    {
-        return new self($signingKey, $recipientKeys, $gnupgHome);
-    }
-
-    /**
-     * @param boolean $encrypt
-     */
-    public function setEncrypt($encrypt)
-    {
-        $this->encrypt = $encrypt;
-    }
-
-    /**
-     * @param string $gnupgHome
-     */
-    public function setGnupgHome($gnupgHome)
-    {
-        $this->gnupgHome = $gnupgHome;
-    }
-
-    /**
-     * @param string $micalg
-     */
-    public function setMicalg($micalg)
-    {
-        $this->micalg = $micalg;
-    }
-
-    /**
-     * @param $identifier
-     * @param null $passPhrase
-     *
-     * @throws Swift_SwiftException
-     */
-    public function addSignature($identifier, $keyFingerprint = null, $passPhrase = null)
-    {
-        if (!$keyFingerprint) {
-            $keyFingerprint   = $this->getKey($identifier, 'sign');
-        }
-        $this->signingKey = $keyFingerprint;
-
-        if ($passPhrase) {
-            $this->addKeyPassphrase($keyFingerprint, $passPhrase);
-        }
-    }
-
-    /**
-     * @param $identifier
-     * @param $passPhrase
-     *
-     * @throws Swift_SwiftException
-     */
-    public function addKeyPassphrase($identifier, $passPhrase)
-    {
-        $keyFingerprint                        = $this->getKey($identifier, 'sign');
-        $this->keyPassphrases[$keyFingerprint] = $passPhrase;
-    }
-
-    /**
-     * Adds a recipient to encrypt a copy of the email for. If you exclude a key fingerprint, we
-     * will try to find a matching key based on the identifier. However if no match is found, or
-     * if multiple valid keys are found, this will fail. Specifying a key fingerprint avoids these
-     * issues.
-     *
-     * @param string $identifier
-     * an email address, but could be a key fingerprint, key ID, name, etc.
-     *
-     * @param string $keyFingerprint
-     */
-    public function addRecipient($identifier, $keyFingerprint = null)
-    {
-        if (!$keyFingerprint) {
-            $keyFingerprint = $this->getKey($identifier, 'encrypt');
-        }
-
-        $this->recipientKeys[$identifier] = $keyFingerprint;
-    }
-
-    /**
-     * @param Swift_Message $message
-     *
-     * @return $this
-     *
-     * @throws Swift_DependencyException
-     * @throws Swift_SwiftException
-     */
-    public function signMessage(Swift_Message $message)
-    {
-        $originalMessage = $this->createMessage($message);
-
-        $message->setChildren([]);
-
-        $message->setEncoder(Swift_DependencyContainer::getInstance()->lookup('mime.rawcontentencoder'));
-
-        $type = $message->getHeaders()->get('Content-Type');
-
-        $type->setValue('multipart/signed');
-
-        $type->setParameters([
-            'micalg'   => sprintf("pgp-%s", strtolower($this->micalg)),
-            'protocol' => 'application/pgp-signature',
-            'boundary' => $message->getBoundary()
-        ]);
-
-        if (!$this->signingKey) {
-            foreach ($message->getFrom() as $key => $value) {
-                $this->addSignature($this->getKey($key, 'sign'));
-            }
-        }
-
-        if (!$this->signingKey) {
-            throw new Swift_SwiftException('Signing has been enabled, but no signature has been added. Use autoAddSignature() or addSignature()');
-        }
-
-        $signedBody = $originalMessage->toString();
-
-        $lines = preg_split('/(\r\n|\r|\n)/', rtrim($signedBody));
-
-        for ($i=0; $i<count($lines); $i++) {
-            $lines[$i] = rtrim($lines[$i])."\r\n";
-        }
-
-        // Remove excess trailing newlines (RFC3156 section 5.4)
-        $signedBody = rtrim(implode('', $lines))."\r\n";
-
-        $signature = $this->pgpSignString($signedBody, $this->signingKey);
-
-        //Swiftmailer is automatically changing content type and this is the hack to prevent it
-        // Fixes DKIM signature incorrect body hash for custom domains
-        $body = "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)\r\n\r\n";
-        $body .= "--{$message->getBoundary()}\r\n";
-        $body .= $signedBody."\r\n";
-        $body .= "--{$message->getBoundary()}\r\n";
-        $body .= "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n";
-        $body .= "Content-Description: OpenPGP digital signature\r\n";
-        $body .= "Content-Disposition: attachment; filename=\"signature.asc\"\r\n\r\n";
-        $body .= $signature."\r\n\r\n";
-        $body .= "--{$message->getBoundary()}--";
-
-        $message->setBody($body);
-
-        if ($this->encrypt) {
-            $signed = sprintf("%s\r\n%s", $message->getHeaders()->get('Content-Type')->toString(), $body);
-
-            if (!$this->recipientKeys) {
-                foreach ($message->getTo() as $key => $value) {
-                    if (!isset($this->recipientKeys[$key])) {
-                        $this->addRecipient($key);
-                    }
-                }
-            }
-
-            if (!$this->recipientKeys) {
-                throw new Swift_SwiftException('Encryption has been enabled, but no recipients have been added. Use autoAddRecipients() or addRecipient()');
-            }
-
-            //Create body from signed message
-            $encryptedBody = $this->pgpEncryptString($signed, array_keys($this->recipientKeys));
-
-            $type = $message->getHeaders()->get('Content-Type');
-
-            $type->setValue('multipart/encrypted');
-
-            $type->setParameters([
-                'protocol' => 'application/pgp-encrypted',
-                'boundary' => $message->getBoundary()
-            ]);
-
-            // Fixes DKIM signature incorrect body hash for custom domains
-            $body = "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\r\n\r\n";
-            $body .= "--{$message->getBoundary()}\r\n";
-            $body .= "Content-Type: application/pgp-encrypted\r\n";
-            $body .= "Content-Description: PGP/MIME version identification\r\n\r\n";
-            $body .= "Version: 1\r\n\r\n";
-            $body .= "--{$message->getBoundary()}\r\n";
-            $body .= "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n";
-            $body .= "Content-Description: OpenPGP encrypted message\r\n";
-            $body .= "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n\r\n";
-            $body .= $encryptedBody."\r\n\r\n";
-            $body .= "--{$message->getBoundary()}--";
-
-            $message->setBody($body);
-        }
-
-        $messageHeaders = $message->getHeaders();
-        $messageHeaders->removeAll('Content-Transfer-Encoding');
-
-        return $this;
-    }
-
-    /**
-     * @return array
-     */
-    public function getAlteredHeaders()
-    {
-        return ['Content-Type', 'Content-Transfer-Encoding', 'Content-Disposition', 'Content-Description'];
-    }
-
-    /**
-     * @return $this
-     */
-    public function reset()
-    {
-        return $this;
-    }
-
-    protected function createMessage(Swift_Message $message)
-    {
-        $mimeEntity = new Swift_Message('', $message->getBody(), $message->getContentType(), $message->getCharset());
-        $mimeEntity->setChildren($message->getChildren());
-
-        $messageHeaders = $mimeEntity->getHeaders();
-        $messageHeaders->remove('Message-ID');
-        $messageHeaders->remove('Date');
-        $messageHeaders->remove('Subject');
-        $messageHeaders->remove('MIME-Version');
-        $messageHeaders->remove('To');
-        $messageHeaders->remove('From');
-
-        return $mimeEntity;
-    }
-
-    /**
-     * @throws Swift_SwiftException
-     */
-    protected function initGNUPG()
-    {
-        if (!class_exists('gnupg')) {
-            throw new Swift_SwiftException('PHPMailerPGP requires the GnuPG class');
-        }
-
-        if (!$this->gnupgHome && isset($_SERVER['HOME'])) {
-            $this->gnupgHome = $_SERVER['HOME'] . '/.gnupg';
-        }
-
-        if (!$this->gnupgHome && getenv('HOME')) {
-            $this->gnupgHome = getenv('HOME') . '/.gnupg';
-        }
-
-        if (!$this->gnupg) {
-            $this->gnupg = new \gnupg();
-        }
-
-        $this->gnupg->seterrormode(\gnupg::ERROR_EXCEPTION);
-    }
-
-    /**
-     * @param $plaintext
-     * @param $keyFingerprint
-     *
-     * @return string
-     *
-     * @throws Swift_SwiftException
-     */
-    protected function pgpSignString($plaintext, $keyFingerprint)
-    {
-        if (isset($this->keyPassphrases[$keyFingerprint]) && !$this->keyPassphrases[$keyFingerprint]) {
-            $passPhrase = $this->keyPassphrases[$keyFingerprint];
-        } else {
-            $passPhrase = null;
-        }
-
-        $this->gnupg->clearsignkeys();
-        $this->gnupg->addsignkey($keyFingerprint, $passPhrase);
-        $this->gnupg->setsignmode(\gnupg::SIG_MODE_DETACH);
-        $this->gnupg->setarmor(1);
-
-        $signed = $this->gnupg->sign($plaintext);
-
-        if ($signed) {
-            return $signed;
-        }
-
-        throw new Swift_SwiftException('Unable to sign message (perhaps the secret key is encrypted with a passphrase?)');
-    }
-
-    /**
-     * @param $plaintext
-     * @param $keyFingerprints
-     *
-     * @return string
-     *
-     * @throws Swift_SwiftException
-     */
-    protected function pgpEncryptString($plaintext, $keyFingerprints)
-    {
-        $this->gnupg->clearencryptkeys();
-
-        foreach ($keyFingerprints as $keyFingerprint) {
-            $this->gnupg->addencryptkey($keyFingerprint);
-        }
-
-        $this->gnupg->setarmor(1);
-
-        $encrypted = $this->gnupg->encrypt($plaintext);
-
-        if ($encrypted) {
-            return $encrypted;
-        }
-
-        throw new Swift_SwiftException('Unable to encrypt message');
-    }
-
-    /**
-     * @param $identifier
-     * @param $purpose
-     *
-     * @return string
-     *
-     * @throws Swift_SwiftException
-     */
-    protected function getKey($identifier, $purpose)
-    {
-        $keys         = $this->gnupg->keyinfo($identifier);
-        $fingerprints = [];
-
-        foreach ($keys as $key) {
-            if ($this->isValidKey($key, $purpose)) {
-                foreach ($key['subkeys'] as $subKey) {
-                    if ($this->isValidKey($subKey, $purpose)) {
-                        $fingerprints[] = $subKey['fingerprint'];
-                    }
-                }
-            }
-        }
-
-        // Return first available to encrypt
-        if (count($fingerprints) >= 1) {
-            return $fingerprints[0];
-        }
-
-        /* if (count($fingerprints) > 1) {
-            throw new Swift_SwiftException(sprintf('Found more than one active key for %s use addRecipient() or addSignature()', $identifier));
-        } */
-
-        throw new Swift_SwiftException(sprintf('Unable to find an active key to %s for %s,try importing keys first', $purpose, $identifier));
-    }
-
-    protected function isValidKey($key, $purpose)
-    {
-        return !($key['disabled'] || $key['expired'] || $key['revoked'] || ($purpose == 'sign' && !$key['can_sign']) || ($purpose == 'encrypt' && !$key['can_encrypt']));
-    }
-}

+ 1 - 3
app/Http/Controllers/AliasExportController.php

@@ -9,8 +9,6 @@ class AliasExportController extends Controller
 {
     public function export()
     {
-        //return (new AliasesExport)->download('aliases.csv', \Maatwebsite\Excel\Excel::CSV);
-
-        return Excel::download(new AliasesExport, 'aliases-'.now()->toDateString().'.csv');
+        return Excel::download(new AliasesExport(), 'aliases-'.now()->toDateString().'.csv');
     }
 }

+ 6 - 4
app/Http/Controllers/Api/AppVersionController.php

@@ -2,18 +2,20 @@
 
 namespace App\Http\Controllers\Api;
 
+use App\Helpers\GitVersionHelper as Version;
 use App\Http\Controllers\Controller;
-use PragmaRX\Version\Package\Facade as Version;
 
 class AppVersionController extends Controller
 {
     public function index()
     {
+        $parts = str(Version::version())->explode('.');
+
         return response()->json([
             'version' => Version::version(),
-            'major' => (int) Version::major(),
-            'minor' => (int) Version::minor(),
-            'patch' => (int) Version::patch()
+            'major' => (int) $parts[0],
+            'minor' => (int) $parts[1],
+            'patch' => (int) $parts[2]
         ]);
     }
 }

+ 4 - 4
app/Http/Controllers/Auth/RegisterController.php

@@ -62,16 +62,16 @@ class RegisterController extends Controller
                 'regex:/^[a-zA-Z0-9]*$/',
                 'max:20',
                 'unique:usernames,username',
-                new NotBlacklisted,
-                new NotDeletedUsername
+                new NotBlacklisted(),
+                new NotDeletedUsername()
             ],
             'email' => [
                 'required',
                 'email:rfc,dns',
                 'max:254',
                 'confirmed',
-                new RegisterUniqueRecipient,
-                new NotLocalRecipient
+                new RegisterUniqueRecipient(),
+                new NotLocalRecipient()
             ],
             'password' => ['required', 'min:8'],
         ], [

+ 11 - 32
app/Http/Controllers/Auth/WebauthnController.php

@@ -2,7 +2,6 @@
 
 namespace App\Http\Controllers\Auth;
 
-use App\Actions\RegisterKeyStore;
 use App\Facades\Webauthn as WebauthnFacade;
 use App\Models\WebauthnKey;
 use Illuminate\Database\Eloquent\ModelNotFoundException;
@@ -10,10 +9,11 @@ use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Redirect;
 use Illuminate\Support\Facades\Response;
 use Illuminate\Support\Str;
-use LaravelWebauthn\Actions\RegisterKeyPrepare;
+use LaravelWebauthn\Actions\PrepareCreationData;
+use LaravelWebauthn\Actions\ValidateKeyCreation;
+use LaravelWebauthn\Contracts\RegisterViewResponse;
 use LaravelWebauthn\Http\Controllers\WebauthnKeyController as ControllersWebauthnController;
-use LaravelWebauthn\Services\Webauthn;
-use Webauthn\PublicKeyCredentialCreationOptions;
+use LaravelWebauthn\Http\Requests\WebauthnRegisterRequest;
 
 class WebauthnController extends ControllersWebauthnController
 {
@@ -22,13 +22,6 @@ class WebauthnController extends ControllersWebauthnController
         return user()->webauthnKeys()->latest()->select(['id','name','enabled','created_at'])->get()->values();
     }
 
-    /**
-     * PublicKey Creation session name.
-     *
-     * @var string
-     */
-    private const SESSION_PUBLICKEY_CREATION = 'webauthn.publicKeyCreation';
-
     /**
      * Return the register data to attempt a Webauthn registration.
      *
@@ -37,49 +30,35 @@ class WebauthnController extends ControllersWebauthnController
      */
     public function create(Request $request)
     {
-        $publicKey = $this->app[RegisterKeyPrepare::class]($request->user());
-
-        $request->session()->put(Webauthn::SESSION_PUBLICKEY_CREATION, $publicKey);
+        $publicKey = app(PrepareCreationData::class)($request->user());
 
-        return view('vendor.webauthn.register')->with('publicKey', $publicKey);
+        return app(RegisterViewResponse::class)
+            ->setPublicKey($request, $publicKey);
     }
 
     /**
      * Validate and create the Webauthn request.
      *
-     * @param \Illuminate\Http\Request $request
+     * @param  WebauthnRegisterRequest  $request
      * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
      */
-    public function store(Request $request)
+    public function store(WebauthnRegisterRequest $request)
     {
         $request->validate([
-            'register' => 'required|string',
             'name' => 'required|string|max:50'
         ]);
 
         try {
-            $publicKey = $request->session()->pull(self::SESSION_PUBLICKEY_CREATION);
-            if (! $publicKey instanceof PublicKeyCredentialCreationOptions) {
-                throw new ModelNotFoundException(trans('webauthn::errors.create_data_not_found'));
-            }
-
-            /** @var \LaravelWebauthn\Models\WebauthnKey|null */
-            $webauthnKey = $this->app[RegisterKeyStore::class](
+            app(ValidateKeyCreation::class)(
                 $request->user(),
-                $publicKey,
-                $request->input('register'),
+                $request->only(['id', 'rawId', 'response', 'type']),
                 $request->input('name')
             );
 
-            if ($webauthnKey !== null) {
-                $request->session()->put(Webauthn::SESSION_WEBAUTHNID_CREATED, $webauthnKey->id);
-            }
-
             user()->update([
                 'two_factor_enabled' => false
             ]);
 
-
             return $this->redirectAfterSuccessRegister();
         } catch (\Exception $e) {
             return Response::json([

+ 3 - 1
app/Http/Controllers/Controller.php

@@ -9,5 +9,7 @@ use Illuminate\Routing\Controller as BaseController;
 
 class Controller extends BaseController
 {
-    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+    use AuthorizesRequests;
+    use DispatchesJobs;
+    use ValidatesRequests;
 }

+ 3 - 0
app/Http/Controllers/SettingController.php

@@ -34,6 +34,9 @@ class SettingController extends Controller
 
         DeleteAccount::dispatch(user());
 
+        auth()->logout();
+        $request->session()->invalidate();
+
         return redirect()->route('login')
         ->with(['status' => 'Account deleted successfully!']);
     }

+ 2 - 1
app/Http/Kernel.php

@@ -16,7 +16,7 @@ class Kernel extends HttpKernel
     protected $middleware = [
         // \App\Http\Middleware\TrustHosts::class,
         \App\Http\Middleware\TrustProxies::class,
-        \Fruitcake\Cors\HandleCors::class,
+        \Illuminate\Http\Middleware\HandleCors::class,
         \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
         \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
         \App\Http\Middleware\TrimStrings::class,
@@ -56,6 +56,7 @@ class Kernel extends HttpKernel
     protected $routeMiddleware = [
         'auth' => \App\Http\Middleware\Authenticate::class,
         'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
+        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
         'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
         'can' => \Illuminate\Auth\Middleware\Authorize::class,
         'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,

+ 7 - 2
app/Http/Middleware/TrustProxies.php

@@ -2,7 +2,7 @@
 
 namespace App\Http\Middleware;
 
-use Fideloper\Proxy\TrustProxies as Middleware;
+use Illuminate\Http\Middleware\TrustProxies as Middleware;
 use Illuminate\Http\Request;
 
 class TrustProxies extends Middleware
@@ -19,5 +19,10 @@ class TrustProxies extends Middleware
      *
      * @var int
      */
-    protected $headers = Request::HEADER_X_FORWARDED_ALL;
+    protected $headers =
+        Request::HEADER_X_FORWARDED_FOR |
+        Request::HEADER_X_FORWARDED_HOST |
+        Request::HEADER_X_FORWARDED_PORT |
+        Request::HEADER_X_FORWARDED_PROTO |
+        Request::HEADER_X_FORWARDED_AWS_ELB;
 }

+ 1 - 1
app/Http/Requests/EditDefaultRecipientRequest.php

@@ -30,7 +30,7 @@ class EditDefaultRecipientRequest extends FormRequest
                 'email:rfc,dns',
                 'max:254',
                 'confirmed',
-                new RegisterUniqueRecipient,
+                new RegisterUniqueRecipient(),
                 'not_in:'.$this->user()->email
             ]
         ];

+ 1 - 1
app/Http/Requests/StoreAliasRecipientRequest.php

@@ -28,7 +28,7 @@ class StoreAliasRecipientRequest extends FormRequest
             'recipient_ids' => [
                 'array',
                 'max:10',
-                new VerifiedRecipientId
+                new VerifiedRecipientId()
             ]
         ];
     }

+ 2 - 2
app/Http/Requests/StoreAliasRequest.php

@@ -38,7 +38,7 @@ class StoreAliasRequest extends FormRequest
                 'nullable',
                 'array',
                 'max:10',
-                new VerifiedRecipientId
+                new VerifiedRecipientId()
             ]
         ];
     }
@@ -52,7 +52,7 @@ class StoreAliasRequest extends FormRequest
                 return $query->where('local_part', $this->validationData()['local_part'])
                 ->where('domain', $this->validationData()['domain']);
             }),
-            new ValidAliasLocalPart
+            new ValidAliasLocalPart()
         ], function () {
             $format = $this->validationData()['format'] ?? 'random_characters';
             return $format === 'custom';

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

@@ -32,9 +32,9 @@ class StoreDomainRequest extends FormRequest
                 'string',
                 'max:50',
                 'unique:domains',
-                new ValidDomain,
-                new NotLocalDomain,
-                new NotUsedAsRecipientDomain
+                new ValidDomain(),
+                new NotLocalDomain(),
+                new NotUsedAsRecipientDomain()
             ]
         ];
     }

+ 2 - 2
app/Http/Requests/StoreRecipientRequest.php

@@ -31,8 +31,8 @@ class StoreRecipientRequest extends FormRequest
                 'string',
                 'max:254',
                 'email:rfc',
-                new UniqueRecipient,
-                new NotLocalRecipient
+                new UniqueRecipient(),
+                new NotLocalRecipient()
             ]
         ];
     }

+ 1 - 1
app/Http/Requests/StoreReorderRuleRequest.php

@@ -28,7 +28,7 @@ class StoreReorderRuleRequest extends FormRequest
             'ids' => [
                 'required',
                 'array',
-                new ValidRuleId
+                new ValidRuleId()
             ]
         ];
     }

+ 2 - 2
app/Http/Requests/StoreUsernameRequest.php

@@ -31,8 +31,8 @@ class StoreUsernameRequest extends FormRequest
                 'regex:/^[a-zA-Z0-9]*$/',
                 'max:20',
                 'unique:usernames,username',
-                new NotBlacklisted,
-                new NotDeletedUsername
+                new NotBlacklisted(),
+                new NotDeletedUsername()
             ],
         ];
     }

+ 4 - 1
app/Jobs/DeleteAccount.php

@@ -12,7 +12,10 @@ use Illuminate\Queue\SerializesModels;
 
 class DeleteAccount implements ShouldQueue, ShouldBeEncrypted
 {
-    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+    use Dispatchable;
+    use InteractsWithQueue;
+    use Queueable;
+    use SerializesModels;
 
     protected $user;
 

+ 31 - 67
app/Mail/ForwardEmail.php

@@ -2,13 +2,11 @@
 
 namespace App\Mail;
 
-use App\Helpers\AlreadyEncryptedSigner;
-use App\Helpers\OpenPGPSigner;
+use App\CustomMailDriver\Mime\Part\InlineImagePart;
 use App\Models\Alias;
 use App\Models\EmailData;
 use App\Models\Recipient;
 use App\Notifications\FailedDeliveryNotification;
-use App\Notifications\GpgKeyExpired;
 use App\Traits\CheckUserRules;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -17,13 +15,13 @@ use Illuminate\Mail\Mailable;
 use Illuminate\Queue\SerializesModels;
 use Illuminate\Support\Facades\URL;
 use Illuminate\Support\Str;
-use Swift_Image;
-use Swift_Signers_DKIMSigner;
-use Swift_SwiftException;
+use Symfony\Component\Mime\Email;
 
 class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
 {
-    use Queueable, SerializesModels, CheckUserRules;
+    use Queueable;
+    use SerializesModels;
+    use CheckUserRules;
 
     protected $email;
     protected $user;
@@ -41,8 +39,6 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
     protected $deactivateUrl;
     protected $bannerLocation;
     protected $fingerprint;
-    protected $openpgpsigner;
-    protected $dkimSigner;
     protected $encryptedParts;
     protected $fromEmail;
     protected $size;
@@ -90,23 +86,9 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $this->encryptedParts = $emailData->encryptedParts ?? null;
         $this->recipientId = $recipient->id;
 
-        $fingerprint = $recipient->should_encrypt && !$this->isAlreadyEncrypted() ? $recipient->fingerprint : null;
+        $this->fingerprint = $recipient->should_encrypt && !$this->isAlreadyEncrypted() ? $recipient->fingerprint : null;
 
         $this->bannerLocation = $this->isAlreadyEncrypted() ? 'off' : $this->alias->user->banner_location;
-
-        if ($this->fingerprint = $fingerprint) {
-            try {
-                $this->openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $this->openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $this->openpgpsigner = null;
-
-                $recipient->update(['should_encrypt' => false]);
-
-                $recipient->notify(new GpgKeyExpired);
-            }
-        }
     }
 
     /**
@@ -128,19 +110,7 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $returnPath = $this->alias->email;
 
         if ($this->alias->isCustomDomain()) {
-            if ($this->alias->aliasable->isVerifiedForSending()) {
-                if (config('anonaddy.dkim_signing_key')) {
-                    $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->ignoreHeader('Feedback-ID');
-                    $this->dkimSigner->ignoreHeader('Content-Type');
-                    $this->dkimSigner->ignoreHeader('Content-Description');
-                    $this->dkimSigner->ignoreHeader('Content-Disposition');
-                    $this->dkimSigner->ignoreHeader('Content-Transfer-Encoding');
-                    $this->dkimSigner->ignoreHeader('MIME-Version');
-                }
-            } else {
+            if (! $this->alias->aliasable->isVerifiedForSending()) {
                 if (! isset($replyToEmail)) {
                     $replyToEmail = $this->fromEmail;
                 }
@@ -153,8 +123,8 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $this->email =  $this
             ->from($this->fromEmail, base64_decode($this->displayFrom)." '".$this->sender."'")
             ->subject($this->user->email_subject ?? base64_decode($this->emailSubject))
-            ->withSwiftMessage(function ($message) use ($returnPath) {
-                $message->setReturnPath($returnPath);
+            ->withSymfonyMessage(function (Email $message) use ($returnPath) {
+                $message->returnPath($returnPath);
 
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'F:' . $this->alias->id . ':anonaddy');
@@ -163,14 +133,14 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
                 $message->getHeaders()
                         ->addTextHeader('Alias-To', $this->alias->email);
 
-                if ($this->messageId) {
-                    $message->getHeaders()->remove('Message-ID');
+                $message->getHeaders()->remove('Message-ID');
 
-                    // We're not using $message->setId here because it can cause RFC exceptions
+                if ($this->messageId) {
                     $message->getHeaders()
-                            ->addTextHeader('Message-ID', base64_decode($this->messageId));
+                            ->addIdHeader('Message-ID', base64_decode($this->messageId));
                 } else {
-                    $message->setId(bin2hex(random_bytes(16)).'@'.$this->alias->domain);
+                    $message->getHeaders()
+                            ->addIdHeader('Message-ID', bin2hex(random_bytes(16)).'@'.$this->alias->domain);
                 }
 
                 if ($this->listUnsubscribe) {
@@ -214,33 +184,17 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
                             ->addTextHeader('Sender', base64_decode($this->originalSenderHeader));
                 }
 
-                if ($this->encryptedParts) {
-                    $alreadyEncryptedSigner = new AlreadyEncryptedSigner($this->encryptedParts);
-
-                    $message->attachSigner($alreadyEncryptedSigner);
-                }
-
-                if ($this->openpgpsigner) {
-                    $message->attachSigner($this->openpgpsigner);
-                }
-
-                if ($this->dkimSigner) {
-                    $message->attachSigner($this->dkimSigner);
-                }
-
                 if ($this->emailInlineAttachments) {
                     foreach ($this->emailInlineAttachments as $attachment) {
-                        $image = new Swift_Image(base64_decode($attachment['stream']), base64_decode($attachment['file_name']), base64_decode($attachment['mime']));
+                        $part = new InlineImagePart(base64_decode($attachment['stream']), base64_decode($attachment['file_name']), base64_decode($attachment['mime']));
 
-                        $cids[] = 'cid:' . base64_decode($attachment['contentId']);
-                        $newCids[] = $message->embed($image);
-                    }
+                        $part->asInline();
 
-                    $message->getHeaders()
-                            ->addTextHeader('X-Old-Cids', implode(',', $cids));
+                        $part->setContentId(base64_decode($attachment['contentId']));
+                        $part->setFileName(base64_decode($attachment['file_name']));
 
-                    $message->getHeaders()
-                            ->addTextHeader('X-New-Cids', implode(',', $newCids));
+                        $message->attachPart($part);
+                    }
                 }
 
                 if ($this->originalCc) {
@@ -289,10 +243,15 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
             'location' => $this->bannerLocation,
             'deactivateUrl' => $this->deactivateUrl,
             'aliasEmail' => $this->alias->email,
+            'aliasDomain' => $this->alias->domain,
             'aliasDescription' => $this->alias->description,
+            'recipientId' => $this->recipientId,
+            'fingerprint' => $this->fingerprint,
+            'encryptedParts' => $this->encryptedParts,
             'fromEmail' => $this->sender,
             'replacedSubject' => $this->replacedSubject,
-            'shouldBlock' => $this->size === 0
+            'shouldBlock' => $this->size === 0,
+            'needsDkimSignature' => $this->needsDkimSignature()
         ]);
 
         if (isset($replyToEmail)) {
@@ -350,4 +309,9 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
     {
         return $this->encryptedParts || preg_match('/^-----BEGIN PGP MESSAGE-----([A-Za-z0-9+=\/\n]+)-----END PGP MESSAGE-----$/', base64_decode($this->emailText));
     }
+
+    private function needsDkimSignature()
+    {
+        return $this->alias->isCustomDomain() ? $this->alias->aliasable->isVerifiedForSending() : false;
+    }
 }

+ 27 - 43
app/Mail/ReplyToEmail.php

@@ -2,7 +2,7 @@
 
 namespace App\Mail;
 
-use App\Helpers\AlreadyEncryptedSigner;
+use App\CustomMailDriver\Mime\Part\InlineImagePart;
 use App\Models\Alias;
 use App\Models\EmailData;
 use App\Models\User;
@@ -13,12 +13,13 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Mail\Mailable;
 use Illuminate\Queue\SerializesModels;
-use Swift_Image;
-use Swift_Signers_DKIMSigner;
+use Symfony\Component\Mime\Email;
 
 class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
 {
-    use Queueable, SerializesModels, CheckUserRules;
+    use Queueable;
+    use SerializesModels;
+    use CheckUserRules;
 
     protected $email;
     protected $user;
@@ -29,7 +30,6 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
     protected $emailHtml;
     protected $emailAttachments;
     protected $emailInlineAttachments;
-    protected $dkimSigner;
     protected $encryptedParts;
     protected $displayFrom;
     protected $fromEmail;
@@ -69,21 +69,7 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $returnPath = $this->alias->email;
 
         if ($this->alias->isCustomDomain()) {
-            if ($this->alias->aliasable->isVerifiedForSending()) {
-                $this->fromEmail = $this->alias->email;
-
-                if (config('anonaddy.dkim_signing_key')) {
-                    $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->ignoreHeader('Feedback-ID');
-                    $this->dkimSigner->ignoreHeader('Content-Type');
-                    $this->dkimSigner->ignoreHeader('Content-Description');
-                    $this->dkimSigner->ignoreHeader('Content-Disposition');
-                    $this->dkimSigner->ignoreHeader('Content-Transfer-Encoding');
-                    $this->dkimSigner->ignoreHeader('MIME-Version');
-                }
-            } else {
+            if (! $this->alias->aliasable->isVerifiedForSending()) {
                 $this->fromEmail = config('mail.from.address');
                 $returnPath = config('anonaddy.return_path');
             }
@@ -94,14 +80,16 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $this->email =  $this
             ->from($this->fromEmail, $this->displayFrom)
             ->subject(base64_decode($this->emailSubject))
-            ->withSwiftMessage(function ($message) use ($returnPath) {
-                $message->setReturnPath($returnPath);
+            ->withSymfonyMessage(function (Email $message) use ($returnPath) {
+                $message->returnPath($returnPath);
 
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'R:' . $this->alias->id . ':anonaddy');
 
                 // Message-ID is replaced on replies as it can leak parts of the real email
-                $message->setId(bin2hex(random_bytes(16)).'@'.$this->alias->domain);
+                $message->getHeaders()->remove('Message-ID');
+                $message->getHeaders()
+                            ->addIdHeader('Message-ID', bin2hex(random_bytes(16)).'@'.$this->alias->domain);
 
                 if ($this->inReplyTo) {
                     $message->getHeaders()
@@ -113,29 +101,17 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
                             ->addTextHeader('References', base64_decode($this->references));
                 }
 
-                if ($this->encryptedParts) {
-                    $alreadyEncryptedSigner = new AlreadyEncryptedSigner($this->encryptedParts);
-
-                    $message->attachSigner($alreadyEncryptedSigner);
-                }
-
-                if ($this->dkimSigner) {
-                    $message->attachSigner($this->dkimSigner);
-                }
-
                 if ($this->emailInlineAttachments) {
                     foreach ($this->emailInlineAttachments as $attachment) {
-                        $image = new Swift_Image(base64_decode($attachment['stream']), base64_decode($attachment['file_name']), base64_decode($attachment['mime']));
+                        $part = new InlineImagePart(base64_decode($attachment['stream']), base64_decode($attachment['file_name']), base64_decode($attachment['mime']));
 
-                        $cids[] = 'cid:' . base64_decode($attachment['contentId']);
-                        $newCids[] = $message->embed($image);
-                    }
+                        $part->asInline();
 
-                    $message->getHeaders()
-                            ->addTextHeader('X-Old-Cids', implode(',', $cids));
+                        $part->setContentId(base64_decode($attachment['contentId']));
+                        $part->setFileName(base64_decode($attachment['file_name']));
 
-                    $message->getHeaders()
-                            ->addTextHeader('X-New-Cids', implode(',', $newCids));
+                        $message->attachPart($part);
+                    }
                 }
             });
 
@@ -169,10 +145,13 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $this->checkRules('Replies');
 
         $this->email->with([
-            'shouldBlock' => $this->size === 0
+            'shouldBlock' => $this->size === 0,
+            'encryptedParts' => $this->encryptedParts,
+            'needsDkimSignature' => $this->needsDkimSignature(),
+            'aliasDomain' => $this->alias->domain
         ]);
 
-        if ($this->alias->isCustomDomain() && !$this->dkimSigner) {
+        if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
             $this->email->replyTo($this->alias->email, $this->displayFrom);
         }
 
@@ -220,4 +199,9 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
             'attempted_at' => now()
         ]);
     }
+
+    private function needsDkimSignature()
+    {
+        return $this->alias->isCustomDomain() ? $this->alias->aliasable->isVerifiedForSending() : false;
+    }
 }

+ 27 - 43
app/Mail/SendFromEmail.php

@@ -2,7 +2,7 @@
 
 namespace App\Mail;
 
-use App\Helpers\AlreadyEncryptedSigner;
+use App\CustomMailDriver\Mime\Part\InlineImagePart;
 use App\Models\Alias;
 use App\Models\EmailData;
 use App\Models\User;
@@ -13,12 +13,13 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Mail\Mailable;
 use Illuminate\Queue\SerializesModels;
-use Swift_Image;
-use Swift_Signers_DKIMSigner;
+use Symfony\Component\Mime\Email;
 
 class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
 {
-    use Queueable, SerializesModels, CheckUserRules;
+    use Queueable;
+    use SerializesModels;
+    use CheckUserRules;
 
     protected $email;
     protected $user;
@@ -29,7 +30,6 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
     protected $emailHtml;
     protected $emailAttachments;
     protected $emailInlineAttachments;
-    protected $dkimSigner;
     protected $encryptedParts;
     protected $displayFrom;
     protected $fromEmail;
@@ -65,21 +65,7 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $returnPath = $this->alias->email;
 
         if ($this->alias->isCustomDomain()) {
-            if ($this->alias->aliasable->isVerifiedForSending()) {
-                $this->fromEmail = $this->alias->email;
-
-                if (config('anonaddy.dkim_signing_key')) {
-                    $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->ignoreHeader('Feedback-ID');
-                    $this->dkimSigner->ignoreHeader('Content-Type');
-                    $this->dkimSigner->ignoreHeader('Content-Description');
-                    $this->dkimSigner->ignoreHeader('Content-Disposition');
-                    $this->dkimSigner->ignoreHeader('Content-Transfer-Encoding');
-                    $this->dkimSigner->ignoreHeader('MIME-Version');
-                }
-            } else {
+            if (! $this->alias->aliasable->isVerifiedForSending()) {
                 $this->fromEmail = config('mail.from.address');
                 $returnPath = config('anonaddy.return_path');
             }
@@ -90,38 +76,28 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $this->email =  $this
             ->from($this->fromEmail, $this->displayFrom)
             ->subject(base64_decode($this->emailSubject))
-            ->withSwiftMessage(function ($message) use ($returnPath) {
-                $message->setReturnPath($returnPath);
+            ->withSymfonyMessage(function (Email $message) use ($returnPath) {
+                $message->returnPath($returnPath);
 
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'S:' . $this->alias->id . ':anonaddy');
 
                 // Message-ID is replaced on send from as it can leak parts of the real email
-                $message->setId(bin2hex(random_bytes(16)).'@'.$this->alias->domain);
-
-                if ($this->encryptedParts) {
-                    $alreadyEncryptedSigner = new AlreadyEncryptedSigner($this->encryptedParts);
-
-                    $message->attachSigner($alreadyEncryptedSigner);
-                }
-
-                if ($this->dkimSigner) {
-                    $message->attachSigner($this->dkimSigner);
-                }
+                $message->getHeaders()->remove('Message-ID');
+                $message->getHeaders()
+                            ->addIdHeader('Message-ID', bin2hex(random_bytes(16)).'@'.$this->alias->domain);
 
                 if ($this->emailInlineAttachments) {
                     foreach ($this->emailInlineAttachments as $attachment) {
-                        $image = new Swift_Image(base64_decode($attachment['stream']), base64_decode($attachment['file_name']), base64_decode($attachment['mime']));
+                        $part = new InlineImagePart(base64_decode($attachment['stream']), base64_decode($attachment['file_name']), base64_decode($attachment['mime']));
 
-                        $cids[] = 'cid:' . base64_decode($attachment['contentId']);
-                        $newCids[] = $message->embed($image);
-                    }
+                        $part->asInline();
 
-                    $message->getHeaders()
-                            ->addTextHeader('X-Old-Cids', implode(',', $cids));
+                        $part->setContentId(base64_decode($attachment['contentId']));
+                        $part->setFileName(base64_decode($attachment['file_name']));
 
-                    $message->getHeaders()
-                            ->addTextHeader('X-New-Cids', implode(',', $newCids));
+                        $message->attachPart($part);
+                    }
                 }
             });
 
@@ -155,10 +131,13 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
         $this->checkRules('Sends');
 
         $this->email->with([
-            'shouldBlock' => $this->size === 0
+            'shouldBlock' => $this->size === 0,
+            'encryptedParts' => $this->encryptedParts,
+            'needsDkimSignature' => $this->needsDkimSignature(),
+            'aliasDomain' => $this->alias->domain
         ]);
 
-        if ($this->alias->isCustomDomain() && !$this->dkimSigner) {
+        if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
             $this->email->replyTo($this->alias->email, $this->displayFrom);
         }
 
@@ -206,4 +185,9 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
             'attempted_at' => now()
         ]);
     }
+
+    private function needsDkimSignature()
+    {
+        return $this->alias->isCustomDomain() ? $this->alias->aliasable->isVerifiedForSending() : false;
+    }
 }

+ 13 - 3
app/Mail/TokenExpiringSoon.php

@@ -8,12 +8,15 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Mail\Mailable;
 use Illuminate\Queue\SerializesModels;
+use Symfony\Component\Mime\Email;
 
 class TokenExpiringSoon extends Mailable implements ShouldQueue, ShouldBeEncrypted
 {
-    use Queueable, SerializesModels;
+    use Queueable;
+    use SerializesModels;
 
     protected $user;
+    protected $recipient;
 
     /**
      * Create a new message instance.
@@ -23,6 +26,7 @@ class TokenExpiringSoon extends Mailable implements ShouldQueue, ShouldBeEncrypt
     public function __construct(User $user)
     {
         $this->user = $user;
+        $this->recipient = $user->defaultRecipient;
     }
 
     /**
@@ -35,7 +39,13 @@ class TokenExpiringSoon extends Mailable implements ShouldQueue, ShouldBeEncrypt
         return $this
             ->subject("Your AnonAddy API token expires soon")
             ->markdown('mail.token_expiring_soon', [
-                'user' => $this->user
-            ]);
+                'user' => $this->user,
+                'recipientId' => $this->recipient->id,
+                'fingerprint' => $this->recipient->should_encrypt ? $this->recipient->fingerprint : null
+            ])
+            ->withSymfonyMessage(function (Email $message) {
+                $message->getHeaders()
+                        ->addTextHeader('Feedback-ID', 'TES:anonaddy');
+            });
     }
 }

+ 4 - 1
app/Models/Alias.php

@@ -11,7 +11,10 @@ use Illuminate\Support\Str;
 
 class Alias extends Model
 {
-    use SoftDeletes, HasUuid, HasEncryptedAttributes, HasFactory;
+    use SoftDeletes;
+    use HasUuid;
+    use HasEncryptedAttributes;
+    use HasFactory;
 
     public $incrementing = false;
 

+ 2 - 1
app/Models/DeletedUsername.php

@@ -8,7 +8,8 @@ use Illuminate\Database\Eloquent\Model;
 
 class DeletedUsername extends Model
 {
-    use HasEncryptedAttributes, HasFactory;
+    use HasEncryptedAttributes;
+    use HasFactory;
 
     public $incrementing = false;
 

+ 3 - 1
app/Models/Domain.php

@@ -11,7 +11,9 @@ use Illuminate\Support\Facades\App;
 
 class Domain extends Model
 {
-    use HasUuid, HasEncryptedAttributes, HasFactory;
+    use HasUuid;
+    use HasEncryptedAttributes;
+    use HasFactory;
 
     public $incrementing = false;
 

+ 2 - 1
app/Models/EmailData.php

@@ -2,6 +2,7 @@
 
 namespace App\Models;
 
+use Illuminate\Support\Str;
 use PhpMimeMailParser\Parser;
 
 class EmailData
@@ -28,7 +29,7 @@ class EmailData
         $this->attachments = [];
         $this->inlineAttachments = [];
         $this->size = $size;
-        $this->messageId = base64_encode($parser->getHeader('Message-ID'));
+        $this->messageId = base64_encode(Str::remove(['<', '>'], $parser->getHeader('Message-ID')));
         $this->listUnsubscribe = base64_encode($parser->getHeader('List-Unsubscribe'));
         $this->inReplyTo = base64_encode($parser->getHeader('In-Reply-To'));
         $this->references = base64_encode($parser->getHeader('References'));

+ 3 - 1
app/Models/FailedDelivery.php

@@ -10,7 +10,9 @@ use Illuminate\Database\Eloquent\Model;
 
 class FailedDelivery extends Model
 {
-    use HasUuid, HasEncryptedAttributes, HasFactory;
+    use HasUuid;
+    use HasEncryptedAttributes;
+    use HasFactory;
 
     public $incrementing = false;
 

+ 6 - 3
app/Models/Recipient.php

@@ -13,7 +13,10 @@ use Illuminate\Notifications\Notifiable;
 
 class Recipient extends Model
 {
-    use Notifiable, HasUuid, HasEncryptedAttributes, HasFactory;
+    use Notifiable;
+    use HasUuid;
+    use HasEncryptedAttributes;
+    use HasFactory;
 
     public $incrementing = false;
 
@@ -140,7 +143,7 @@ class Recipient extends Model
      */
     public function sendEmailVerificationNotification()
     {
-        $this->notify(new CustomVerifyEmail);
+        $this->notify(new CustomVerifyEmail());
     }
 
     /**
@@ -150,7 +153,7 @@ class Recipient extends Model
      */
     public function sendUsernameReminderNotification()
     {
-        $this->notify(new UsernameReminder);
+        $this->notify(new UsernameReminder());
     }
 
     /**

+ 2 - 1
app/Models/Rule.php

@@ -8,7 +8,8 @@ use Illuminate\Database\Eloquent\Model;
 
 class Rule extends Model
 {
-    use HasUuid, HasFactory;
+    use HasUuid;
+    use HasFactory;
 
     public $incrementing = false;
 

+ 7 - 3
app/Models/User.php

@@ -16,7 +16,11 @@ use Laravel\Passport\HasApiTokens;
 
 class User extends Authenticatable implements MustVerifyEmail
 {
-    use Notifiable, HasUuid, HasEncryptedAttributes, HasApiTokens, HasFactory;
+    use Notifiable;
+    use HasUuid;
+    use HasEncryptedAttributes;
+    use HasApiTokens;
+    use HasFactory;
 
     public $incrementing = false;
 
@@ -316,7 +320,7 @@ class User extends Authenticatable implements MustVerifyEmail
      */
     public function sendEmailVerificationNotification()
     {
-        $this->notify(new CustomVerifyEmail);
+        $this->notify(new CustomVerifyEmail());
     }
 
     public function hasVerifiedDefaultRecipient()
@@ -465,7 +469,7 @@ class User extends Authenticatable implements MustVerifyEmail
             ->implode('.').mt_rand(0, 999);
     }
 
-    public function generateRandomCharacterLocalPart(int $length) : string
+    public function generateRandomCharacterLocalPart(int $length): string
     {
         $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
 

+ 3 - 1
app/Models/Username.php

@@ -9,7 +9,9 @@ use Illuminate\Database\Eloquent\Model;
 
 class Username extends Model
 {
-    use HasUuid, HasEncryptedAttributes, HasFactory;
+    use HasUuid;
+    use HasEncryptedAttributes;
+    use HasFactory;
 
     public $incrementing = false;
 

+ 3 - 2
app/Notifications/CustomVerifyEmail.php

@@ -13,6 +13,7 @@ use Illuminate\Support\Facades\Config;
 use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Facades\Lang;
 use Illuminate\Support\Facades\URL;
+use Symfony\Component\Mime\Email;
 
 class CustomVerifyEmail extends VerifyEmail implements ShouldQueue, ShouldBeEncrypted
 {
@@ -35,13 +36,13 @@ class CustomVerifyEmail extends VerifyEmail implements ShouldQueue, ShouldBeEncr
         $feedbackId = $notifiable instanceof User ? 'VU:anonaddy' : 'VR:anonaddy';
         $recipientId = $notifiable instanceof User ? $notifiable->default_recipient_id : $notifiable->id;
 
-        return (new MailMessage)
+        return (new MailMessage())
             ->subject(Lang::get('Verify Email Address'))
             ->markdown('mail.verify_email', [
                 'verificationUrl' => $verificationUrl,
                 'recipientId' => $recipientId
             ])
-            ->withSwiftMessage(function ($message) use ($feedbackId) {
+            ->withSymfonyMessage(function (Email $message) use ($feedbackId) {
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', $feedbackId);
             });

+ 6 - 26
app/Notifications/DefaultRecipientUpdated.php

@@ -2,13 +2,12 @@
 
 namespace App\Notifications;
 
-use App\Helpers\OpenPGPSigner;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
-use Swift_SwiftException;
+use Symfony\Component\Mime\Email;
 
 class DefaultRecipientUpdated extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -45,36 +44,17 @@ class DefaultRecipientUpdated extends Notification implements ShouldQueue, Shoul
      */
     public function toMail($notifiable)
     {
-        $openpgpsigner = null;
-        $fingerprint = $notifiable->should_encrypt ? $notifiable->fingerprint : null;
-
-        if ($fingerprint) {
-            try {
-                $openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $openpgpsigner = null;
-
-                $notifiable->update(['should_encrypt' => false]);
-
-                $notifiable->notify(new GpgKeyExpired);
-            }
-        }
-
-        return (new MailMessage)
+        return (new MailMessage())
             ->subject("Your default recipient has just been updated")
             ->markdown('mail.default_recipient_updated', [
                 'defaultRecipient' => $notifiable->email,
-                'newDefaultRecipient' => $this->newDefaultRecipient
+                'newDefaultRecipient' => $this->newDefaultRecipient,
+                'recipientId' => $notifiable->id,
+                'fingerprint' => $notifiable->should_encrypt ? $notifiable->fingerprint : null
             ])
-            ->withSwiftMessage(function ($message) use ($openpgpsigner) {
+            ->withSymfonyMessage(function (Email $message) {
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'DRU:anonaddy');
-
-                if ($openpgpsigner) {
-                    $message->attachSigner($openpgpsigner);
-                }
             });
     }
 

+ 6 - 24
app/Notifications/DisallowedReplySendAttempt.php

@@ -2,14 +2,13 @@
 
 namespace App\Notifications;
 
-use App\Helpers\OpenPGPSigner;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
 use Illuminate\Support\Str;
-use Swift_SwiftException;
+use Symfony\Component\Mime\Email;
 
 class DisallowedReplySendAttempt extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -52,38 +51,21 @@ class DisallowedReplySendAttempt extends Notification implements ShouldQueue, Sh
      */
     public function toMail($notifiable)
     {
-        $openpgpsigner = null;
         $fingerprint = $notifiable->should_encrypt ? $notifiable->fingerprint : null;
 
-        if ($fingerprint) {
-            try {
-                $openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $openpgpsigner = null;
-
-                $notifiable->update(['should_encrypt' => false]);
-
-                $notifiable->notify(new GpgKeyExpired);
-            }
-        }
-
-        return (new MailMessage)
+        return (new MailMessage())
             ->subject('Disallowed reply/send from alias')
             ->markdown('mail.disallowed_reply_send_attempt', [
                 'aliasEmail' => $this->aliasEmail,
                 'recipient' => $this->recipient,
                 'destination' => $this->destination,
-                'authenticationResults' => $this->authenticationResults
+                'authenticationResults' => $this->authenticationResults,
+                'recipientId' => $notifiable->id,
+                'fingerprint' => $fingerprint
             ])
-            ->withSwiftMessage(function ($message) use ($openpgpsigner) {
+            ->withSymfonyMessage(function (Email $message) {
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'DRSA:anonaddy');
-
-                if ($openpgpsigner) {
-                    $message->attachSigner($openpgpsigner);
-                }
             });
     }
 

+ 6 - 24
app/Notifications/DomainMxRecordsInvalid.php

@@ -2,13 +2,12 @@
 
 namespace App\Notifications;
 
-use App\Helpers\OpenPGPSigner;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
-use Swift_SwiftException;
+use Symfony\Component\Mime\Email;
 
 class DomainMxRecordsInvalid extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -45,36 +44,19 @@ class DomainMxRecordsInvalid extends Notification implements ShouldQueue, Should
      */
     public function toMail($notifiable)
     {
-        $openpgpsigner = null;
         $recipient = $notifiable->defaultRecipient;
         $fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
 
-        if ($fingerprint) {
-            try {
-                $openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $openpgpsigner = null;
-
-                $recipient->update(['should_encrypt' => false]);
-
-                $recipient->notify(new GpgKeyExpired);
-            }
-        }
-
-        return (new MailMessage)
+        return (new MailMessage())
             ->subject("Your domain's MX records no longer point to AnonAddy")
             ->markdown('mail.domain_mx_records_invalid', [
-                'domain' => $this->domain
+                'domain' => $this->domain,
+                'recipientId' => $recipient->_id,
+                'fingerprint' => $fingerprint
             ])
-            ->withSwiftMessage(function ($message) use ($openpgpsigner) {
+            ->withSymfonyMessage(function (Email $message) {
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'DMI:anonaddy');
-
-                if ($openpgpsigner) {
-                    $message->attachSigner($openpgpsigner);
-                }
             });
     }
 

+ 6 - 24
app/Notifications/DomainUnverifiedForSending.php

@@ -2,13 +2,12 @@
 
 namespace App\Notifications;
 
-use App\Helpers\OpenPGPSigner;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
-use Swift_SwiftException;
+use Symfony\Component\Mime\Email;
 
 class DomainUnverifiedForSending extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -47,37 +46,20 @@ class DomainUnverifiedForSending extends Notification implements ShouldQueue, Sh
      */
     public function toMail($notifiable)
     {
-        $openpgpsigner = null;
         $recipient = $notifiable->defaultRecipient;
         $fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
 
-        if ($fingerprint) {
-            try {
-                $openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $openpgpsigner = null;
-
-                $recipient->update(['should_encrypt' => false]);
-
-                $recipient->notify(new GpgKeyExpired);
-            }
-        }
-
-        return (new MailMessage)
+        return (new MailMessage())
             ->subject("Your domain has been unverified for sending on AnonAddy")
             ->markdown('mail.domain_unverified_for_sending', [
                 'domain' => $this->domain,
-                'reason' => $this->reason
+                'reason' => $this->reason,
+                'recipientId' => $recipient->id,
+                'fingerprint' => $fingerprint
             ])
-            ->withSwiftMessage(function ($message) use ($openpgpsigner) {
+            ->withSymfonyMessage(function (Email $message) {
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'DUS:anonaddy');
-
-                if ($openpgpsigner) {
-                    $message->attachSigner($openpgpsigner);
-                }
             });
     }
 

+ 5 - 26
app/Notifications/FailedDeliveryNotification.php

@@ -2,13 +2,11 @@
 
 namespace App\Notifications;
 
-use App\Helpers\OpenPGPSigner;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
-use Swift_SwiftException;
 
 class FailedDeliveryNotification extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -49,37 +47,18 @@ class FailedDeliveryNotification extends Notification implements ShouldQueue, Sh
      */
     public function toMail($notifiable)
     {
-        $openpgpsigner = null;
-        $fingerprint = $notifiable->should_encrypt ? $notifiable->fingerprint : null;
-
-        if ($fingerprint) {
-            try {
-                $openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $openpgpsigner = null;
-
-                $notifiable->update(['should_encrypt' => false]);
-
-                $notifiable->notify(new GpgKeyExpired);
-            }
-        }
-
-        return (new MailMessage)
+        return (new MailMessage())
                     ->subject("New failed delivery on AnonAddy")
                     ->markdown('mail.failed_delivery_notification', [
                         'aliasEmail' => $this->aliasEmail,
                         'originalSender' => $this->originalSender,
-                        'originalSubject' => $this->originalSubject
+                        'originalSubject' => $this->originalSubject,
+                        'recipientId' => $notifiable->id,
+                        'fingerprint' => $notifiable->should_encrypt ? $notifiable->fingerprint : null
                     ])
-                    ->withSwiftMessage(function ($message) use ($openpgpsigner) {
+                    ->withSymfonyMessage(function ($message) {
                         $message->getHeaders()
                                 ->addTextHeader('Feedback-ID', 'FDN:anonaddy');
-
-                        if ($openpgpsigner) {
-                            $message->attachSigner($openpgpsigner);
-                        }
                     });
     }
 

+ 7 - 2
app/Notifications/GpgKeyExpired.php

@@ -7,6 +7,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use Symfony\Component\Mime\Email;
 
 class GpgKeyExpired extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -31,11 +32,15 @@ class GpgKeyExpired extends Notification implements ShouldQueue, ShouldBeEncrypt
      */
     public function toMail($notifiable)
     {
-        return (new MailMessage)
+        return (new MailMessage())
             ->subject("Your GPG key has expired on AnonAddy")
             ->markdown('mail.gpg_key_expired', [
                 'recipient' => $notifiable
-            ]);
+            ])
+            ->withSymfonyMessage(function (Email $message) {
+                $message->getHeaders()
+                        ->addTextHeader('Feedback-ID', 'GKE:anonaddy');
+            });
     }
 
     /**

+ 6 - 24
app/Notifications/NearBandwidthLimit.php

@@ -2,13 +2,12 @@
 
 namespace App\Notifications;
 
-use App\Helpers\OpenPGPSigner;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
-use Swift_SwiftException;
+use Symfony\Component\Mime\Email;
 
 class NearBandwidthLimit extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -47,39 +46,22 @@ class NearBandwidthLimit extends Notification implements ShouldQueue, ShouldBeEn
      */
     public function toMail($notifiable)
     {
-        $openpgpsigner = null;
         $recipient = $notifiable->defaultRecipient;
         $fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
 
-        if ($fingerprint) {
-            try {
-                $openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $openpgpsigner = null;
-
-                $recipient->update(['should_encrypt' => false]);
-
-                $recipient->notify(new GpgKeyExpired);
-            }
-        }
-
-        return (new MailMessage)
+        return (new MailMessage())
         ->subject("You're close to your bandwidth limit for ".$this->month)
         ->markdown('mail.near_bandwidth_limit', [
             'bandwidthUsage' => $notifiable->bandwidth_mb,
             'bandwidthLimit' => $notifiable->getBandwidthLimitMb(),
             'month' => $this->month,
-            'reset' => $this->reset
+            'reset' => $this->reset,
+            'recipientId' => $recipient->id,
+            'fingerprint' => $fingerprint
         ])
-        ->withSwiftMessage(function ($message) use ($openpgpsigner) {
+        ->withSymfonyMessage(function (Email $message) {
             $message->getHeaders()
                     ->addTextHeader('Feedback-ID', 'NBL:anonaddy');
-
-            if ($openpgpsigner) {
-                $message->attachSigner($openpgpsigner);
-            }
         });
     }
 

+ 6 - 26
app/Notifications/SpamReplySendAttempt.php

@@ -2,14 +2,13 @@
 
 namespace App\Notifications;
 
-use App\Helpers\OpenPGPSigner;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
 use Illuminate\Support\Str;
-use Swift_SwiftException;
+use Symfony\Component\Mime\Email;
 
 class SpamReplySendAttempt extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -52,38 +51,19 @@ class SpamReplySendAttempt extends Notification implements ShouldQueue, ShouldBe
      */
     public function toMail($notifiable)
     {
-        $openpgpsigner = null;
-        $fingerprint = $notifiable->should_encrypt ? $notifiable->fingerprint : null;
-
-        if ($fingerprint) {
-            try {
-                $openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $openpgpsigner = null;
-
-                $notifiable->update(['should_encrypt' => false]);
-
-                $notifiable->notify(new GpgKeyExpired);
-            }
-        }
-
-        return (new MailMessage)
+        return (new MailMessage())
             ->subject('Attempted reply/send from alias has failed')
             ->markdown('mail.spam_reply_send_attempt', [
                 'aliasEmail' => $this->aliasEmail,
                 'recipient' => $this->recipient,
                 'destination' => $this->destination,
-                'authenticationResults' => $this->authenticationResults
+                'authenticationResults' => $this->authenticationResults,
+                'recipientId' => $notifiable->id,
+                'fingerprint' => $notifiable->should_encrypt ? $notifiable->fingerprint : null
             ])
-            ->withSwiftMessage(function ($message) use ($openpgpsigner) {
+            ->withSymfonyMessage(function (Email $message) {
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'SRSA:anonaddy');
-
-                if ($openpgpsigner) {
-                    $message->attachSigner($openpgpsigner);
-                }
             });
     }
 

+ 6 - 26
app/Notifications/UsernameReminder.php

@@ -2,13 +2,12 @@
 
 namespace App\Notifications;
 
-use App\Helpers\OpenPGPSigner;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
-use Swift_SwiftException;
+use Symfony\Component\Mime\Email;
 
 class UsernameReminder extends Notification implements ShouldQueue, ShouldBeEncrypted
 {
@@ -33,35 +32,16 @@ class UsernameReminder extends Notification implements ShouldQueue, ShouldBeEncr
      */
     public function toMail($notifiable)
     {
-        $openpgpsigner = null;
-        $fingerprint = $notifiable->should_encrypt ? $notifiable->fingerprint : null;
-
-        if ($fingerprint) {
-            try {
-                $openpgpsigner = OpenPGPSigner::newInstance(config('anonaddy.signing_key_fingerprint'), [], "~/.gnupg");
-                $openpgpsigner->addRecipient($fingerprint);
-            } catch (Swift_SwiftException $e) {
-                info($e->getMessage());
-                $openpgpsigner = null;
-
-                $notifiable->update(['should_encrypt' => false]);
-
-                $notifiable->notify(new GpgKeyExpired);
-            }
-        }
-
-        return (new MailMessage)
+        return (new MailMessage())
             ->subject("AnonAddy Username Reminder")
             ->markdown('mail.username_reminder', [
-                'username' => $notifiable->user->username
+                'username' => $notifiable->user->username,
+                'recipientId' => $notifiable->id,
+                'fingerprint' => $notifiable->should_encrypt ? $notifiable->fingerprint : null
             ])
-            ->withSwiftMessage(function ($message) use ($openpgpsigner) {
+            ->withSymfonyMessage(function (Email $message) {
                 $message->getHeaders()
                         ->addTextHeader('Feedback-ID', 'UR:anonaddy');
-
-                if ($openpgpsigner) {
-                    $message->attachSigner($openpgpsigner);
-                }
             });
     }
 

+ 0 - 3
app/Providers/AppServiceProvider.php

@@ -8,7 +8,6 @@ use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Blade;
 use Illuminate\Support\ServiceProvider;
-use Swift_Preferences;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -31,8 +30,6 @@ class AppServiceProvider extends ServiceProvider
     {
         Blade::withoutComponentTags();
 
-        Swift_Preferences::getInstance()->setQPDotEscape(true);
-
         Builder::macro('jsonPaginate', function (int $maxResults = null, int $defaultSize = null) {
             $maxResults = $maxResults ?? 100;
             $defaultSize = $defaultSize ?? 100;

+ 3 - 2
app/Providers/CustomMailServiceProvider.php

@@ -3,6 +3,7 @@
 namespace App\Providers;
 
 use App\CustomMailDriver\CustomMailManager;
+use Illuminate\Contracts\Foundation\Application;
 use Illuminate\Mail\MailServiceProvider;
 
 class CustomMailServiceProvider extends MailServiceProvider
@@ -14,11 +15,11 @@ class CustomMailServiceProvider extends MailServiceProvider
      */
     protected function registerIlluminateMailer()
     {
-        $this->app->singleton('mail.manager', function ($app) {
+        $this->app->singleton('mail.manager', static function (Application $app) {
             return new CustomMailManager($app);
         });
 
-        $this->app->bind('mailer', function ($app) {
+        $this->app->bind('mailer', static function (Application $app) {
             return $app->make('mail.manager')->mailer();
         });
     }

+ 10 - 0
app/Providers/EventServiceProvider.php

@@ -33,4 +33,14 @@ class EventServiceProvider extends ServiceProvider
     {
         //
     }
+
+    /**
+     * Determine if events and listeners should be automatically discovered.
+     *
+     * @return bool
+     */
+    public function shouldDiscoverEvents()
+    {
+        return false;
+    }
 }

+ 1 - 0
app/Providers/HelperServiceProvider.php

@@ -14,6 +14,7 @@ class HelperServiceProvider extends ServiceProvider
     public function register()
     {
         require_once(app_path().'/Helpers/Helper.php');
+        require_once(app_path().'/Helpers/GitVersionHelper.php');
     }
 
     /**

+ 5 - 5
app/Providers/RouteServiceProvider.php

@@ -22,14 +22,14 @@ class RouteServiceProvider extends ServiceProvider
     /**
      * The path to the "home" route for your application.
      *
-     * This is used by Laravel authentication to redirect users after login.
+     * Typically, users are redirected here after authentication.s
      *
      * @var string
      */
     public const HOME = '/';
 
     /**
-     * Define your route model bindings, pattern filters, etc.
+     * Define your route model bindings, pattern filters, and other route configuration.
      *
      * @return void
      */
@@ -38,9 +38,9 @@ class RouteServiceProvider extends ServiceProvider
         $this->configureRateLimiting();
 
         $this->routes(function () {
-            Route::prefix('api')
-                ->middleware('api')
+            Route::middleware('api')
                 ->namespace($this->namespace)
+                ->prefix('api')
                 ->group(base_path('routes/api.php'));
 
             Route::middleware('web')
@@ -57,7 +57,7 @@ class RouteServiceProvider extends ServiceProvider
     protected function configureRateLimiting()
     {
         RateLimiter::for('api', function (Request $request) {
-            return Limit::perMinute(60);
+            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
         });
     }
 }

+ 3 - 23
app/Services/Webauthn.php

@@ -5,38 +5,18 @@ namespace App\Services;
 use App\Models\WebauthnKey;
 use Illuminate\Contracts\Auth\Authenticatable as User;
 use LaravelWebauthn\Services\Webauthn as ServicesWebauthn;
-use Webauthn\PublicKeyCredentialSource;
 
 class Webauthn extends ServicesWebauthn
 {
-    /**
-     * Create a new key.
-     *
-     * @param  User  $user
-     * @param  string  $keyName
-     * @param  PublicKeyCredentialSource  $publicKeyCredentialSource
-     * @return WebauthnKey
-     */
-    public function create(User $user, string $keyName, PublicKeyCredentialSource $publicKeyCredentialSource)
-    {
-        $webauthnKey = new WebauthnKey();
-        $webauthnKey->user_id = $user->getAuthIdentifier();
-        $webauthnKey->name = $keyName;
-        $webauthnKey->publicKeyCredentialSource = $publicKeyCredentialSource;
-        $webauthnKey->save();
-
-        return $webauthnKey;
-    }
-
     /**
      * Test if the user has one or more webauthn key.
      *
      * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
      * @return bool
      */
-    public function enabled(User $user): bool
+    public static function enabled(User $user): bool
     {
-        return $this->webauthnEnabled() && $this->hasKey($user);
+        return static::webauthnEnabled() && static::hasKey($user);
     }
 
     /**
@@ -45,7 +25,7 @@ class Webauthn extends ServicesWebauthn
      * @param User $user
      * @return bool
      */
-    public function hasKey(User $user): bool
+    public static function hasKey(User $user): bool
     {
         return WebauthnKey::where('user_id', $user->getAuthIdentifier())->where('enabled', true)->count() > 0;
     }

+ 2 - 5
app/Traits/CheckUserRules.php

@@ -118,11 +118,8 @@ trait CheckUserRules
                 break;
             case 'encryption':
                 if ($action['value'] == false) {
-                    // detach the openpgpsigner from the email...
-                    if (isset($this->openpgpsigner)) {
-                        $this->email->withSwiftMessage(function ($message) {
-                            $message->detachSigner($this->openpgpsigner);
-                        });
+                    if (isset($this->fingerprint)) {
+                        $this->fingerprint = null;
                     }
                 }
                 break;

+ 10 - 14
composer.json

@@ -7,32 +7,28 @@
     ],
     "license": "MIT",
     "require": {
-        "php": "^7.3|^8.0",
-        "asbiin/laravel-webauthn": "^2.0.0",
+        "php": "^8.0.2",
+        "asbiin/laravel-webauthn": "^3.0.0",
         "bacon/bacon-qr-code": "^2.0",
         "doctrine/dbal": "^3.0",
-        "fideloper/proxy": "^4.2",
-        "fruitcake/laravel-cors": "^2.0",
-        "guzzlehttp/guzzle": "^7.0.1",
-        "laravel/framework": "^8.0",
+        "guzzlehttp/guzzle": "^7.2",
+        "laravel/framework": "^9.11",
         "laravel/passport": "^10.0",
-        "laravel/tinker": "^2.0",
+        "laravel/tinker": "^2.7",
         "laravel/ui": "^3.0",
         "maatwebsite/excel": "^3.1",
         "mews/captcha": "^3.0.0",
         "php-mime-mail-parser/php-mime-mail-parser": "^7.0",
         "pragmarx/google2fa-laravel": "^2.0.0",
-        "pragmarx/version": "^1.2",
         "ramsey/uuid": "^4.0"
     },
     "require-dev": {
-        "beyondcode/laravel-dump-server": "^1.0",
-        "facade/ignition": "^2.3.6",
         "fakerphp/faker": "^1.9.1",
         "friendsofphp/php-cs-fixer": "^3.0.0",
-        "mockery/mockery": "^1.3.1",
-        "nunomaduro/collision": "^5.0",
-        "phpunit/phpunit": "^9.3"
+        "mockery/mockery": "^1.4.4",
+        "nunomaduro/collision": "^6.1",
+        "phpunit/phpunit": "^9.5.10",
+        "spatie/laravel-ignition": "^1.0"
     },
     "config": {
         "optimize-autoloader": true,
@@ -62,7 +58,7 @@
         "post-autoload-dump": [
             "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
             "@php artisan package:discover --ansi",
-            "@php artisan version:absorb --ansi"
+            "@php artisan anonaddy:update-app-version"
         ],
         "post-root-package-install": [
             "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

File diff ditekan karena terlalu besar
+ 213 - 423
composer.lock


+ 25 - 38
config/app.php

@@ -1,5 +1,7 @@
 <?php
 
+use Illuminate\Support\Facades\Facade;
+
 return [
 
     /*
@@ -39,7 +41,7 @@ return [
     |
     */
 
-    'debug' => env('APP_DEBUG', false),
+    'debug' => (bool) env('APP_DEBUG', false),
 
     /*
     |--------------------------------------------------------------------------
@@ -54,7 +56,7 @@ return [
 
     'url' => env('APP_URL', 'http://localhost'),
 
-    'asset_url' => env('ASSET_URL', null),
+    'asset_url' => env('ASSET_URL'),
 
     /*
     |--------------------------------------------------------------------------
@@ -123,6 +125,24 @@ return [
 
     'cipher' => 'AES-256-CBC',
 
+    /*
+    |--------------------------------------------------------------------------
+    | Maintenance Mode Driver
+    |--------------------------------------------------------------------------
+    |
+    | These configuration options determine the driver used to determine and
+    | manage Laravel's "maintenance mode" status. The "cache" driver will
+    | allow maintenance mode to be controlled across multiple machines.
+    |
+    | Supported drivers: "file", "cache"
+    |
+    */
+
+    'maintenance' => [
+        'driver' => 'file',
+        // 'store'  => 'redis',
+    ],
+
     /*
     |--------------------------------------------------------------------------
     | Autoloaded Service Providers
@@ -190,41 +210,8 @@ return [
     |
     */
 
-    'aliases' => [
-
-        'App' => Illuminate\Support\Facades\App::class,
-        'Artisan' => Illuminate\Support\Facades\Artisan::class,
-        'Auth' => Illuminate\Support\Facades\Auth::class,
-        'Blade' => Illuminate\Support\Facades\Blade::class,
-        'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
-        'Bus' => Illuminate\Support\Facades\Bus::class,
-        'Cache' => Illuminate\Support\Facades\Cache::class,
-        'Config' => Illuminate\Support\Facades\Config::class,
-        'Cookie' => Illuminate\Support\Facades\Cookie::class,
-        'Crypt' => Illuminate\Support\Facades\Crypt::class,
-        'DB' => Illuminate\Support\Facades\DB::class,
-        'Eloquent' => Illuminate\Database\Eloquent\Model::class,
-        'Event' => Illuminate\Support\Facades\Event::class,
-        'File' => Illuminate\Support\Facades\File::class,
-        'Gate' => Illuminate\Support\Facades\Gate::class,
-        'Hash' => Illuminate\Support\Facades\Hash::class,
-        'Lang' => Illuminate\Support\Facades\Lang::class,
-        'Log' => Illuminate\Support\Facades\Log::class,
-        'Mail' => Illuminate\Support\Facades\Mail::class,
-        'Notification' => Illuminate\Support\Facades\Notification::class,
-        'Password' => Illuminate\Support\Facades\Password::class,
-        'Queue' => Illuminate\Support\Facades\Queue::class,
-        'Redirect' => Illuminate\Support\Facades\Redirect::class,
-        'Request' => Illuminate\Support\Facades\Request::class,
-        'Response' => Illuminate\Support\Facades\Response::class,
-        'Route' => Illuminate\Support\Facades\Route::class,
-        'Schema' => Illuminate\Support\Facades\Schema::class,
-        'Session' => Illuminate\Support\Facades\Session::class,
-        'Storage' => Illuminate\Support\Facades\Storage::class,
-        'URL' => Illuminate\Support\Facades\URL::class,
-        'Validator' => Illuminate\Support\Facades\Validator::class,
-        'View' => Illuminate\Support\Facades\View::class,
-
-    ],
+    'aliases' => Facade::defaultAliases()->merge([
+        // 'ExampleClass' => App\Example\ExampleClass::class,
+    ])->toArray(),
 
 ];

+ 10 - 2
config/broadcasting.php

@@ -11,7 +11,7 @@ return [
     | framework when an event needs to be broadcast. You may set this to
     | any of the connections defined in the "connections" array below.
     |
-    | Supported: "pusher", "redis", "log", "null"
+    | Supported: "pusher", "ably", "redis", "log", "null"
     |
     */
 
@@ -37,8 +37,16 @@ return [
             'app_id' => env('PUSHER_APP_ID'),
             'options' => [
                 'cluster' => env('PUSHER_APP_CLUSTER'),
-                'encrypted' => true,
+                'useTLS' => true,
             ],
+            'client_options' => [
+                // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
+            ],
+        ],
+
+        'ably' => [
+            'driver' => 'ably',
+            'key' => env('ABLY_KEY'),
         ],
 
         'redis' => [

+ 3 - 3
config/cache.php

@@ -82,9 +82,9 @@ return [
     | Cache Key Prefix
     |--------------------------------------------------------------------------
     |
-    | When utilizing a RAM based store such as APC or Memcached, there might
-    | be other applications utilizing the same cache. So, we'll specify a
-    | value to get prefixed to all our keys so we can avoid collisions.
+    | When utilizing the APC, database, memcached, Redis, or DynamoDB cache
+    | stores there might be other applications using the same cache. For
+    | that reason, you may prefix every cache key to avoid collisions.
     |
     */
 

+ 13 - 1
config/database.php

@@ -1,5 +1,7 @@
 <?php
 
+use Illuminate\Support\Str;
+
 return [
 
     /*
@@ -54,6 +56,9 @@ return [
             'prefix_indexes' => true,
             'strict' => true,
             'engine' => null,
+            'options' => extension_loaded('pdo_mysql') ? array_filter([
+                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
+            ]) : [],
         ],
 
         'pgsql' => [
@@ -66,7 +71,7 @@ return [
             'charset' => 'utf8',
             'prefix' => '',
             'prefix_indexes' => true,
-            'schema' => 'public',
+            'search_path' => 'public',
             'sslmode' => 'prefer',
         ],
 
@@ -80,6 +85,8 @@ return [
             'charset' => 'utf8',
             'prefix' => '',
             'prefix_indexes' => true,
+            // 'encrypt' => env('DB_ENCRYPT', 'yes'),
+            // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
         ],
 
     ],
@@ -112,6 +119,11 @@ return [
 
         'client' => env('REDIS_CLIENT', 'phpredis'),
 
+        'options' => [
+            'cluster' => env('REDIS_CLUSTER', 'redis'),
+            'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
+        ],
+
         'default' => [
             'host' => env('REDIS_HOST', '127.0.0.1'),
             'password' => env('REDIS_PASSWORD', null),

+ 5 - 2
config/filesystems.php

@@ -13,7 +13,7 @@ return [
     |
     */
 
-    'default' => env('FILESYSTEM_DRIVER', 'local'),
+    'default' => env('FILESYSTEM_DISK', 'local'),
 
     /*
     |--------------------------------------------------------------------------
@@ -35,7 +35,7 @@ return [
     |
     | Here you may configure as many filesystem "disks" as you wish, and you
     | may even configure multiple disks of the same driver. Defaults have
-    | been setup for each driver as an example of the required options.
+    | been set up for each driver as an example of the required values.
     |
     | Supported Drivers: "local", "ftp", "sftp", "s3", "rackspace"
     |
@@ -46,6 +46,7 @@ return [
         'local' => [
             'driver' => 'local',
             'root' => storage_path('app'),
+            'throw' => false,
         ],
 
         'public' => [
@@ -53,6 +54,7 @@ return [
             'root' => storage_path('app/public'),
             'url' => env('APP_URL').'/storage',
             'visibility' => 'public',
+            'throw' => false,
         ],
 
         's3' => [
@@ -62,6 +64,7 @@ return [
             'region' => env('AWS_DEFAULT_REGION'),
             'bucket' => env('AWS_BUCKET'),
             'url' => env('AWS_URL'),
+            'throw' => false,
         ],
 
     ],

+ 29 - 2
config/logging.php

@@ -1,5 +1,6 @@
 <?php
 
+use Monolog\Handler\NullHandler;
 use Monolog\Handler\StreamHandler;
 use Monolog\Handler\SyslogUdpHandler;
 
@@ -18,6 +19,22 @@ return [
 
     'default' => env('LOG_CHANNEL', 'stack'),
 
+    /*
+    |--------------------------------------------------------------------------
+    | Deprecations Log Channel
+    |--------------------------------------------------------------------------
+    |
+    | This option controls the log channel that should be used to log warnings
+    | regarding deprecated PHP and library features. This allows you to get
+    | your application ready for upcoming major versions of dependencies.
+    |
+    */
+
+    'deprecations' => [
+        'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
+        'trace' => false,
+    ],
+
     /*
     |--------------------------------------------------------------------------
     | Log Channels
@@ -63,11 +80,12 @@ return [
 
         'papertrail' => [
             'driver' => 'monolog',
-            'level' => 'debug',
-            'handler' => SyslogUdpHandler::class,
+            'level' => env('LOG_LEVEL', 'debug'),
+            'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
             'handler_with' => [
                 'host' => env('PAPERTRAIL_URL'),
                 'port' => env('PAPERTRAIL_PORT'),
+                'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
             ],
         ],
 
@@ -89,6 +107,15 @@ return [
             'driver' => 'errorlog',
             'level' => 'debug',
         ],
+
+        'null' => [
+            'driver' => 'monolog',
+            'handler' => NullHandler::class,
+        ],
+
+        'emergency' => [
+            'path' => storage_path('logs/laravel.log'),
+        ],
     ],
 
 ];

+ 61 - 79
config/mail.php

@@ -4,45 +4,82 @@ return [
 
     /*
     |--------------------------------------------------------------------------
-    | Mail Driver
+    | Default Mailer
     |--------------------------------------------------------------------------
     |
-    | Laravel supports both SMTP and PHP's "mail" function as drivers for the
-    | sending of e-mail. You may specify which one you're using throughout
-    | your application here. By default, Laravel is setup for SMTP mail.
-    |
-    | Supported: "smtp", "sendmail", "mailgun", "mandrill", "ses",
-    |            "sparkpost", "log", "array"
+    | This option controls the default mailer that is used to send any email
+    | messages sent by your application. Alternative mailers may be setup
+    | and used as needed; however, this mailer will be used by default.
     |
     */
 
-    'driver' => env('MAIL_DRIVER', 'smtp'),
+    'default' => env('MAIL_MAILER', 'smtp'),
 
     /*
     |--------------------------------------------------------------------------
-    | SMTP Host Address
+    | Mailer Configurations
     |--------------------------------------------------------------------------
     |
-    | Here you may provide the host address of the SMTP server used by your
-    | applications. A default option is provided that is compatible with
-    | the Mailgun mail service which will provide reliable deliveries.
+    | Here you may configure all of the mailers used by your application plus
+    | their respective settings. Several examples have been configured for
+    | you and you are free to add your own as your application requires.
     |
-    */
-
-    'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
-
-    /*
-    |--------------------------------------------------------------------------
-    | SMTP Host Port
-    |--------------------------------------------------------------------------
+    | Laravel supports a variety of mail "transport" drivers to be used while
+    | sending an e-mail. You will specify which one you are using for your
+    | mailers below. You are free to add additional mailers as required.
     |
-    | This is the SMTP port used by your application to deliver e-mails to
-    | users of the application. Like the host we have set this value to
-    | stay compatible with the Mailgun e-mail application by default.
+    | Supported: "smtp", "sendmail", "mailgun", "ses",
+    |            "postmark", "log", "array", "failover"
     |
     */
 
-    'port' => env('MAIL_PORT', 587),
+    'mailers' => [
+        'smtp' => [
+            'transport' => 'smtp',
+            'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
+            'port' => env('MAIL_PORT', 587),
+            'encryption' => env('MAIL_ENCRYPTION', 'tls'),
+            'username' => env('MAIL_USERNAME'),
+            'password' => env('MAIL_PASSWORD'),
+            'timeout' => null,
+            'local_domain' => env('MAIL_EHLO_DOMAIN'),
+            'verify_peer' => env('MAIL_VERIFY_PEER', false),
+        ],
+
+        'ses' => [
+            'transport' => 'ses',
+        ],
+
+        'mailgun' => [
+            'transport' => 'mailgun',
+        ],
+
+        'postmark' => [
+            'transport' => 'postmark',
+        ],
+
+        'sendmail' => [
+            'transport' => 'sendmail',
+            'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
+        ],
+
+        'log' => [
+            'transport' => 'log',
+            'channel' => env('MAIL_LOG_CHANNEL'),
+        ],
+
+        'array' => [
+            'transport' => 'array',
+        ],
+
+        'failover' => [
+            'transport' => 'failover',
+            'mailers' => [
+                'smtp',
+                'log',
+            ],
+        ],
+    ],
 
     /*
     |--------------------------------------------------------------------------
@@ -60,47 +97,6 @@ return [
         'name' => env('MAIL_FROM_NAME', 'Example'),
     ],
 
-    /*
-    |--------------------------------------------------------------------------
-    | E-Mail Encryption Protocol
-    |--------------------------------------------------------------------------
-    |
-    | Here you may specify the encryption protocol that should be used when
-    | the application send e-mail messages. A sensible default using the
-    | transport layer security protocol should provide great security.
-    |
-    */
-
-    'encryption' => env('MAIL_ENCRYPTION', 'tls'),
-
-    /*
-    |--------------------------------------------------------------------------
-    | SMTP Server Username
-    |--------------------------------------------------------------------------
-    |
-    | If your SMTP server requires a username for authentication, you should
-    | set it here. This will get used to authenticate with your server on
-    | connection. You may also set the "password" value below this one.
-    |
-    */
-
-    'username' => env('MAIL_USERNAME'),
-
-    'password' => env('MAIL_PASSWORD'),
-
-    /*
-    |--------------------------------------------------------------------------
-    | Sendmail System Path
-    |--------------------------------------------------------------------------
-    |
-    | When using the "sendmail" driver to send e-mails, we will need to know
-    | the path to where Sendmail lives on this server. A default path has
-    | been provided here, which will work well on most of your systems.
-    |
-    */
-
-    'sendmail' => '/usr/sbin/sendmail -bs',
-
     /*
     |--------------------------------------------------------------------------
     | Markdown Mail Settings
@@ -119,18 +115,4 @@ return [
             resource_path('views/vendor/mail'),
         ],
     ],
-
-    /*
-    |--------------------------------------------------------------------------
-    | Log Channel
-    |--------------------------------------------------------------------------
-    |
-    | If you are using the "log" driver, you may specify the logging channel
-    | if you prefer to keep mail messages separate from other log entries
-    | for simpler reading. Otherwise, the default channel will be used.
-    |
-    */
-
-    'log_channel' => env('MAIL_LOG_CHANNEL'),
-
 ];

+ 1 - 0
config/services.php

@@ -18,6 +18,7 @@ return [
         'domain' => env('MAILGUN_DOMAIN'),
         'secret' => env('MAILGUN_SECRET'),
         'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
+        'scheme' => 'https',
     ],
 
     'ses' => [

+ 0 - 60
config/version.yml

@@ -1,60 +0,0 @@
-mode: absorb
-blade-directive: version
-current:
-  label: v
-  major: 0
-  minor: 11
-  patch: 2
-  prerelease: 5-ge800fa7
-  buildmetadata: ''
-  commit: e800fa
-  timestamp:
-    year: 2020
-    month: 10
-    day: 16
-    hour: 11
-    minute: 8
-    second: 24
-    timezone: UTC
-commit:
-  mode: absorb
-  length: 6
-  increment-by: 1
-git:
-  from: local
-  commit:
-    local: 'git rev-parse --verify HEAD'
-    remote: 'git ls-remote {$repository}'
-  branch: refs/heads/master
-  repository: ''
-  version:
-    local: 'git describe'
-    remote: 'git ls-remote {$repository} | grep tags/ | grep -v {} | cut -d / -f 3 | sort --version-sort | tail -1'
-    matcher: '/^(?P<label>[v|V]*[er]*[sion]*)[\.|\s]*(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/'
-  timestamp:
-    local: 'git show -s --format=%ci'
-    remote: 'git show -s --format=%ci origin/master'
-format:
-  regex:
-    optional_bracket: '\[(?P<prefix>.*?)(?P<spaces>\s*)(?P<delimiter>\?\=)(?P<optional>.*?)\]'
-  label: '{$label}'
-  major: '{$major}'
-  minor: '{$minor}'
-  patch: '{$patch}'
-  prerelease: '{$prerelease}'
-  buildmetadata: '{$buildmetadata}'
-  commit: '{$commit}'
-  release: 'v{$major}.{$minor}.{$patch}'
-  version: '{$major}.{$minor}.{$patch}'
-  version-only: 'version {$major}.{$minor}.{$patch}'
-  full: '{$version-only}[.?={$prerelease}][+?={$buildmetadata}] (commit {$commit})'
-  compact: 'v{$major}.{$minor}.{$patch}-{$commit}'
-  timestamp-year: '{$timestamp.year}'
-  timestamp-month: '{$timestamp.month}'
-  timestamp-day: '{$timestamp.day}'
-  timestamp-hour: '{$timestamp.hour}'
-  timestamp-minute: '{$timestamp.minute}'
-  timestamp-second: '{$timestamp.second}'
-  timestamp-timezone: '{$timestamp.timezone}'
-  timestamp-datetime: '{$timestamp.year}-{$timestamp.month}-{$timestamp.day} {$timestamp.hour}:{$timestamp.minute}:{$timestamp.second}'
-  timestamp-full: '{$timestamp.year}-{$timestamp.month}-{$timestamp.day} {$timestamp.hour}:{$timestamp.minute}:{$timestamp.second} {$timestamp.timezone}'

+ 69 - 12
config/webauthn.php

@@ -1,5 +1,7 @@
 <?php
 
+use App\Models\WebauthnKey;
+
 return [
 
     /*
@@ -15,31 +17,84 @@ return [
 
     /*
     |--------------------------------------------------------------------------
-    | Route Middleware
+    | Webauthn Guard
     |--------------------------------------------------------------------------
     |
-    | These middleware will be assigned to Webauthn routes, giving you
-    | the chance to add your own middleware to this list or change any of
-    | the existing middleware. Or, you can simply stick with this list.
+    | Here you may specify which authentication guard Webauthn will use while
+    | authenticating users. This value should correspond with one of your
+    | guards that is already present in your "auth" configuration file.
     |
     */
 
-    'middleware' => [
-        'web',
-        'auth',
-    ],
+    'guard' => 'web',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Username / Email
+    |--------------------------------------------------------------------------
+    |
+    | This value defines which model attribute should be considered as your
+    | application's "username" field. Typically, this might be the email
+    | address of the users but you are free to change this value here.
+    |
+    */
+
+    'username' => 'email',
 
     /*
     |--------------------------------------------------------------------------
-    | Prefix path
+    | Webauthn Routes Prefix / Subdomain
     |--------------------------------------------------------------------------
     |
-    | The uri prefix for all webauthn requests.
+    | Here you may specify which prefix Webauthn will assign to all the routes
+    | that it registers with the application. If necessary, you may change
+    | subdomain under which all of the Webauthn routes will be available.
     |
     */
 
     'prefix' => 'webauthn',
 
+    'domain' => null,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn Routes Middleware
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify which middleware Webauthn will assign to the routes
+    | that it registers with the application. If necessary, you may change
+    | these middleware but typically this provided default is preferred.
+    |
+    */
+
+    'middleware' => ['web'],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn key model
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify the model used to create Webauthn keys.
+    |
+    */
+
+    'model' => WebauthnKey::class,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Rate Limiting
+    |--------------------------------------------------------------------------
+    |
+    | By default, Laravel Webauthn will throttle logins to five requests per
+    | minute for every email and IP address combination. However, if you would
+    | like to specify a custom rate limiter to call then you may specify it here.
+    |
+    */
+
+    'limiters' => [
+        'login' => null,
+    ],
+
     /*
     |--------------------------------------------------------------------------
     | Redirect routes
@@ -67,6 +122,8 @@ return [
     | - authenticate: when a user login, and has to validate Webauthn 2nd factor.
     | - register: when a user request to create a Webauthn key.
     |
+    | If the views are empty or null, then the route will not be registered.
+    |
     */
 
     'views' => [
@@ -83,7 +140,7 @@ return [
     |
     */
 
-    'sessionName' => 'webauthn_auth',
+    'session_name' => 'webauthn_auth',
 
     /*
     |--------------------------------------------------------------------------
@@ -226,6 +283,6 @@ return [
     |
     */
 
-    'userless' => 'null',
+    'userless' => null,
 
 ];

+ 31 - 0
database/migrations/2022_06_29_103709_update_email_type_in_failed_deliveries_table.php

@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class () extends Migration {
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('failed_deliveries', function (Blueprint $table) {
+            $table->string('email_type', 5)->nullable()->change();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('failed_deliveries', function (Blueprint $table) {
+            $table->string('email_type', 3)->nullable()->change();
+        });
+    }
+};

+ 152 - 110
package-lock.json

@@ -102,12 +102,12 @@
             }
         },
         "node_modules/@babel/generator": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.6.tgz",
-            "integrity": "sha512-AIwwoOS8axIC5MZbhNHRLKi3D+DMpvDf9XUcu3pIVAfOHFT45f4AoDAltRbHIQomCipkCZxrNkfpOEHhJz/VKw==",
+            "version": "7.18.7",
+            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.7.tgz",
+            "integrity": "sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==",
             "dependencies": {
-                "@babel/types": "^7.18.6",
-                "@jridgewell/gen-mapping": "^0.3.0",
+                "@babel/types": "^7.18.7",
+                "@jridgewell/gen-mapping": "^0.3.2",
                 "jsesc": "^2.5.1"
             },
             "engines": {
@@ -1625,9 +1625,9 @@
             }
         },
         "node_modules/@babel/types": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.6.tgz",
-            "integrity": "sha512-NdBNzPDwed30fZdDQtVR7ZgaO4UKjuaQFH9VArS+HMnurlOY0JWN+4ROlu/iapMFwjRQU4pOG4StZfDmulEwGA==",
+            "version": "7.18.7",
+            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.7.tgz",
+            "integrity": "sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ==",
             "dependencies": {
                 "@babel/helper-validator-identifier": "^7.18.6",
                 "to-fast-properties": "^2.0.0"
@@ -1666,9 +1666,9 @@
             }
         },
         "node_modules/@jridgewell/resolve-uri": {
-            "version": "3.0.8",
-            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.8.tgz",
-            "integrity": "sha512-YK5G9LaddzGbcucK4c8h5tWFmMPBvRZ/uyWmN1/SbBdIvqGUdWGkJ5BAaccgs6XbzVLsqbPJrBSFwKv3kT9i7w==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
             "engines": {
                 "node": ">=6.0.0"
             }
@@ -1852,18 +1852,18 @@
             }
         },
         "node_modules/@types/eslint": {
-            "version": "8.4.3",
-            "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.3.tgz",
-            "integrity": "sha512-YP1S7YJRMPs+7KZKDb9G63n8YejIwW9BALq7a5j2+H4yl6iOv9CB29edho+cuFRrvmJbbaH2yiVChKLJVysDGw==",
+            "version": "8.4.5",
+            "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz",
+            "integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==",
             "dependencies": {
                 "@types/estree": "*",
                 "@types/json-schema": "*"
             }
         },
         "node_modules/@types/eslint-scope": {
-            "version": "3.7.3",
-            "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz",
-            "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==",
+            "version": "3.7.4",
+            "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
+            "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
             "dependencies": {
                 "@types/eslint": "*",
                 "@types/estree": "*"
@@ -1969,9 +1969,9 @@
             "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="
         },
         "node_modules/@types/node": {
-            "version": "18.0.0",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz",
-            "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA=="
+            "version": "18.0.3",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz",
+            "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ=="
         },
         "node_modules/@types/parse-json": {
             "version": "4.0.0",
@@ -2031,6 +2031,16 @@
                 "@types/node": "*"
             }
         },
+        "node_modules/@vue/compiler-sfc": {
+            "version": "2.7.3",
+            "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.3.tgz",
+            "integrity": "sha512-/9SO6KeR1ljo+j4Tdkl9zsFG2yY4NQ9p3GKdPVCU99bmkNkTW8g9Tq8SMEvhOfo+RhBC0PdDAOmibl+N37f5YA==",
+            "dependencies": {
+                "@babel/parser": "^7.18.4",
+                "postcss": "^8.4.14",
+                "source-map": "^0.6.1"
+            }
+        },
         "node_modules/@vue/component-compiler-utils": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz",
@@ -2858,9 +2868,9 @@
             }
         },
         "node_modules/browserslist": {
-            "version": "4.21.0",
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.0.tgz",
-            "integrity": "sha512-UQxE0DIhRB5z/zDz9iA03BOfxaN2+GQdBYH/2WrSIWEUrnpzTPJbhqt+umq6r3acaPRTW1FNTkrcp0PXgtFkvA==",
+            "version": "4.21.1",
+            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.1.tgz",
+            "integrity": "sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==",
             "funding": [
                 {
                     "type": "opencollective",
@@ -2872,10 +2882,10 @@
                 }
             ],
             "dependencies": {
-                "caniuse-lite": "^1.0.30001358",
-                "electron-to-chromium": "^1.4.164",
+                "caniuse-lite": "^1.0.30001359",
+                "electron-to-chromium": "^1.4.172",
                 "node-releases": "^2.0.5",
-                "update-browserslist-db": "^1.0.0"
+                "update-browserslist-db": "^1.0.4"
             },
             "bin": {
                 "browserslist": "cli.js"
@@ -2966,9 +2976,9 @@
             }
         },
         "node_modules/caniuse-lite": {
-            "version": "1.0.30001359",
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001359.tgz",
-            "integrity": "sha512-Xln/BAsPzEuiVLgJ2/45IaqD9jShtk3Y33anKb4+yLwQzws3+v6odKfpgES/cDEaZMLzSChpIGdbOYtH9MyuHw==",
+            "version": "1.0.30001363",
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz",
+            "integrity": "sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg==",
             "funding": [
                 {
                     "type": "opencollective",
@@ -3182,9 +3192,9 @@
             }
         },
         "node_modules/collect.js": {
-            "version": "4.34.0",
-            "resolved": "https://registry.npmjs.org/collect.js/-/collect.js-4.34.0.tgz",
-            "integrity": "sha512-WoXbKDghKWb1lnN1ScBs/MR7BvOpyE5kI0Q9+k8rFtShLFpgjosYE5YplGKxg/DDSkPXgWzgdNZAEnFUffw1xg=="
+            "version": "4.34.3",
+            "resolved": "https://registry.npmjs.org/collect.js/-/collect.js-4.34.3.tgz",
+            "integrity": "sha512-aFr67xDazPwthsGm729mnClgNuh15JEagU6McKBKqxuHOkWL7vMFzGbhsXDdPZ+H6ia5QKIMGYuGOMENBHnVpg=="
         },
         "node_modules/color-convert": {
             "version": "2.0.1",
@@ -3291,9 +3301,9 @@
             "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
         },
         "node_modules/connect-history-api-fallback": {
-            "version": "1.6.0",
-            "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
-            "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+            "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
             "engines": {
                 "node": ">=0.8"
             }
@@ -3705,6 +3715,11 @@
                 "node": ">=8.0.0"
             }
         },
+        "node_modules/csstype": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
+            "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
+        },
         "node_modules/date-fns": {
             "version": "2.28.0",
             "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
@@ -4003,9 +4018,9 @@
             "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
         },
         "node_modules/electron-to-chromium": {
-            "version": "1.4.172",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.172.tgz",
-            "integrity": "sha512-yDoFfTJnqBAB6hSiPvzmsBJSrjOXJtHSJoqJdI/zSIh7DYupYnIOHt/bbPw/WE31BJjNTybDdNAs21gCMnTh0Q=="
+            "version": "1.4.182",
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.182.tgz",
+            "integrity": "sha512-OpEjTADzGoXABjqobGhpy0D2YsTncAax7IkER68ycc4adaq0dqEG9//9aenKPy7BGA90bqQdLac0dPp6uMkcSg=="
         },
         "node_modules/elliptic": {
             "version": "6.5.4",
@@ -4048,9 +4063,9 @@
             }
         },
         "node_modules/enhanced-resolve": {
-            "version": "5.9.3",
-            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz",
-            "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==",
+            "version": "5.10.0",
+            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz",
+            "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==",
             "dependencies": {
                 "graceful-fs": "^4.2.4",
                 "tapable": "^2.2.0"
@@ -8622,9 +8637,13 @@
             "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
         },
         "node_modules/vue": {
-            "version": "2.6.14",
-            "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
-            "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
+            "version": "2.7.3",
+            "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.3.tgz",
+            "integrity": "sha512-7MTirXG7LYJi3r2jeYy7EiXIhEE85uP3kBwSxqwDsvIfy/l7+qR4U6ajw8ji1KoP+wYYg7ZY28TBTf3P3Fa4Nw==",
+            "dependencies": {
+                "@vue/compiler-sfc": "2.7.3",
+                "csstype": "^3.1.0"
+            }
         },
         "node_modules/vue-good-table": {
             "version": "2.21.11",
@@ -8641,9 +8660,9 @@
             "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog=="
         },
         "node_modules/vue-loader": {
-            "version": "15.9.8",
-            "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.8.tgz",
-            "integrity": "sha512-GwSkxPrihfLR69/dSV3+5CdMQ0D+jXg8Ma1S4nQXKJAznYFX14vHdc/NetQc34Dw+rBbIJyP7JOuVb9Fhprvog==",
+            "version": "15.10.0",
+            "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.0.tgz",
+            "integrity": "sha512-VU6tuO8eKajrFeBzMssFUP9SvakEeeSi1BxdTH5o3+1yUyrldp8IERkSdXlMI2t4kxF2sqYUDsQY+WJBxzBmZg==",
             "dependencies": {
                 "@vue/component-compiler-utils": "^3.1.0",
                 "hash-sum": "^1.0.2",
@@ -8739,12 +8758,12 @@
             }
         },
         "node_modules/vue-template-compiler": {
-            "version": "2.6.14",
-            "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz",
-            "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==",
+            "version": "2.7.3",
+            "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.3.tgz",
+            "integrity": "sha512-QKcV4vj9akZ2zSD1+F7tci3aXpRe5DXU3TZ/ZWJPE9gInXD5UoV+Q2PrCaplj4IGIK8JXRHYaSvOXkOn7tg9Og==",
             "dependencies": {
                 "de-indent": "^1.0.2",
-                "he": "^1.1.0"
+                "he": "^1.2.0"
             }
         },
         "node_modules/vue-template-es2015-compiler": {
@@ -8944,9 +8963,9 @@
             }
         },
         "node_modules/webpack-dev-server": {
-            "version": "4.9.2",
-            "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.2.tgz",
-            "integrity": "sha512-H95Ns95dP24ZsEzO6G9iT+PNw4Q7ltll1GfJHV4fKphuHWgKFzGHWi4alTlTnpk1SPPk41X+l2RB7rLfIhnB9Q==",
+            "version": "4.9.3",
+            "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz",
+            "integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==",
             "dependencies": {
                 "@types/bonjour": "^3.5.9",
                 "@types/connect-history-api-fallback": "^1.3.5",
@@ -8960,7 +8979,7 @@
                 "chokidar": "^3.5.3",
                 "colorette": "^2.0.10",
                 "compression": "^1.7.4",
-                "connect-history-api-fallback": "^1.6.0",
+                "connect-history-api-fallback": "^2.0.0",
                 "default-gateway": "^6.0.3",
                 "express": "^4.17.3",
                 "graceful-fs": "^4.2.6",
@@ -8984,6 +9003,10 @@
             "engines": {
                 "node": ">= 12.13.0"
             },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/webpack"
+            },
             "peerDependencies": {
                 "webpack": "^4.37.0 || ^5.0.0"
             },
@@ -9330,12 +9353,12 @@
             }
         },
         "@babel/generator": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.6.tgz",
-            "integrity": "sha512-AIwwoOS8axIC5MZbhNHRLKi3D+DMpvDf9XUcu3pIVAfOHFT45f4AoDAltRbHIQomCipkCZxrNkfpOEHhJz/VKw==",
+            "version": "7.18.7",
+            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.7.tgz",
+            "integrity": "sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==",
             "requires": {
-                "@babel/types": "^7.18.6",
-                "@jridgewell/gen-mapping": "^0.3.0",
+                "@babel/types": "^7.18.7",
+                "@jridgewell/gen-mapping": "^0.3.2",
                 "jsesc": "^2.5.1"
             },
             "dependencies": {
@@ -10367,9 +10390,9 @@
             }
         },
         "@babel/types": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.6.tgz",
-            "integrity": "sha512-NdBNzPDwed30fZdDQtVR7ZgaO4UKjuaQFH9VArS+HMnurlOY0JWN+4ROlu/iapMFwjRQU4pOG4StZfDmulEwGA==",
+            "version": "7.18.7",
+            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.7.tgz",
+            "integrity": "sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ==",
             "requires": {
                 "@babel/helper-validator-identifier": "^7.18.6",
                 "to-fast-properties": "^2.0.0"
@@ -10396,9 +10419,9 @@
             }
         },
         "@jridgewell/resolve-uri": {
-            "version": "3.0.8",
-            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.8.tgz",
-            "integrity": "sha512-YK5G9LaddzGbcucK4c8h5tWFmMPBvRZ/uyWmN1/SbBdIvqGUdWGkJ5BAaccgs6XbzVLsqbPJrBSFwKv3kT9i7w=="
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
         },
         "@jridgewell/set-array": {
             "version": "1.1.2",
@@ -10559,18 +10582,18 @@
             }
         },
         "@types/eslint": {
-            "version": "8.4.3",
-            "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.3.tgz",
-            "integrity": "sha512-YP1S7YJRMPs+7KZKDb9G63n8YejIwW9BALq7a5j2+H4yl6iOv9CB29edho+cuFRrvmJbbaH2yiVChKLJVysDGw==",
+            "version": "8.4.5",
+            "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz",
+            "integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==",
             "requires": {
                 "@types/estree": "*",
                 "@types/json-schema": "*"
             }
         },
         "@types/eslint-scope": {
-            "version": "3.7.3",
-            "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz",
-            "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==",
+            "version": "3.7.4",
+            "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
+            "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
             "requires": {
                 "@types/eslint": "*",
                 "@types/estree": "*"
@@ -10676,9 +10699,9 @@
             "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="
         },
         "@types/node": {
-            "version": "18.0.0",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz",
-            "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA=="
+            "version": "18.0.3",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz",
+            "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ=="
         },
         "@types/parse-json": {
             "version": "4.0.0",
@@ -10738,6 +10761,16 @@
                 "@types/node": "*"
             }
         },
+        "@vue/compiler-sfc": {
+            "version": "2.7.3",
+            "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.3.tgz",
+            "integrity": "sha512-/9SO6KeR1ljo+j4Tdkl9zsFG2yY4NQ9p3GKdPVCU99bmkNkTW8g9Tq8SMEvhOfo+RhBC0PdDAOmibl+N37f5YA==",
+            "requires": {
+                "@babel/parser": "^7.18.4",
+                "postcss": "^8.4.14",
+                "source-map": "^0.6.1"
+            }
+        },
         "@vue/component-compiler-utils": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz",
@@ -11406,14 +11439,14 @@
             }
         },
         "browserslist": {
-            "version": "4.21.0",
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.0.tgz",
-            "integrity": "sha512-UQxE0DIhRB5z/zDz9iA03BOfxaN2+GQdBYH/2WrSIWEUrnpzTPJbhqt+umq6r3acaPRTW1FNTkrcp0PXgtFkvA==",
+            "version": "4.21.1",
+            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.1.tgz",
+            "integrity": "sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==",
             "requires": {
-                "caniuse-lite": "^1.0.30001358",
-                "electron-to-chromium": "^1.4.164",
+                "caniuse-lite": "^1.0.30001359",
+                "electron-to-chromium": "^1.4.172",
                 "node-releases": "^2.0.5",
-                "update-browserslist-db": "^1.0.0"
+                "update-browserslist-db": "^1.0.4"
             }
         },
         "buffer": {
@@ -11486,9 +11519,9 @@
             }
         },
         "caniuse-lite": {
-            "version": "1.0.30001359",
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001359.tgz",
-            "integrity": "sha512-Xln/BAsPzEuiVLgJ2/45IaqD9jShtk3Y33anKb4+yLwQzws3+v6odKfpgES/cDEaZMLzSChpIGdbOYtH9MyuHw=="
+            "version": "1.0.30001363",
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz",
+            "integrity": "sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg=="
         },
         "chalk": {
             "version": "4.1.2",
@@ -11630,9 +11663,9 @@
             }
         },
         "collect.js": {
-            "version": "4.34.0",
-            "resolved": "https://registry.npmjs.org/collect.js/-/collect.js-4.34.0.tgz",
-            "integrity": "sha512-WoXbKDghKWb1lnN1ScBs/MR7BvOpyE5kI0Q9+k8rFtShLFpgjosYE5YplGKxg/DDSkPXgWzgdNZAEnFUffw1xg=="
+            "version": "4.34.3",
+            "resolved": "https://registry.npmjs.org/collect.js/-/collect.js-4.34.3.tgz",
+            "integrity": "sha512-aFr67xDazPwthsGm729mnClgNuh15JEagU6McKBKqxuHOkWL7vMFzGbhsXDdPZ+H6ia5QKIMGYuGOMENBHnVpg=="
         },
         "color-convert": {
             "version": "2.0.1",
@@ -11725,9 +11758,9 @@
             "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
         },
         "connect-history-api-fallback": {
-            "version": "1.6.0",
-            "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
-            "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg=="
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+            "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA=="
         },
         "consola": {
             "version": "2.15.3",
@@ -12027,6 +12060,11 @@
                 "css-tree": "^1.1.2"
             }
         },
+        "csstype": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
+            "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
+        },
         "date-fns": {
             "version": "2.28.0",
             "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
@@ -12248,9 +12286,9 @@
             "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
         },
         "electron-to-chromium": {
-            "version": "1.4.172",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.172.tgz",
-            "integrity": "sha512-yDoFfTJnqBAB6hSiPvzmsBJSrjOXJtHSJoqJdI/zSIh7DYupYnIOHt/bbPw/WE31BJjNTybDdNAs21gCMnTh0Q=="
+            "version": "1.4.182",
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.182.tgz",
+            "integrity": "sha512-OpEjTADzGoXABjqobGhpy0D2YsTncAax7IkER68ycc4adaq0dqEG9//9aenKPy7BGA90bqQdLac0dPp6uMkcSg=="
         },
         "elliptic": {
             "version": "6.5.4",
@@ -12289,9 +12327,9 @@
             "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
         },
         "enhanced-resolve": {
-            "version": "5.9.3",
-            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz",
-            "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==",
+            "version": "5.10.0",
+            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz",
+            "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==",
             "requires": {
                 "graceful-fs": "^4.2.4",
                 "tapable": "^2.2.0"
@@ -15545,9 +15583,13 @@
             "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
         },
         "vue": {
-            "version": "2.6.14",
-            "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
-            "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
+            "version": "2.7.3",
+            "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.3.tgz",
+            "integrity": "sha512-7MTirXG7LYJi3r2jeYy7EiXIhEE85uP3kBwSxqwDsvIfy/l7+qR4U6ajw8ji1KoP+wYYg7ZY28TBTf3P3Fa4Nw==",
+            "requires": {
+                "@vue/compiler-sfc": "2.7.3",
+                "csstype": "^3.1.0"
+            }
         },
         "vue-good-table": {
             "version": "2.21.11",
@@ -15564,9 +15606,9 @@
             "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog=="
         },
         "vue-loader": {
-            "version": "15.9.8",
-            "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.8.tgz",
-            "integrity": "sha512-GwSkxPrihfLR69/dSV3+5CdMQ0D+jXg8Ma1S4nQXKJAznYFX14vHdc/NetQc34Dw+rBbIJyP7JOuVb9Fhprvog==",
+            "version": "15.10.0",
+            "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.0.tgz",
+            "integrity": "sha512-VU6tuO8eKajrFeBzMssFUP9SvakEeeSi1BxdTH5o3+1yUyrldp8IERkSdXlMI2t4kxF2sqYUDsQY+WJBxzBmZg==",
             "requires": {
                 "@vue/component-compiler-utils": "^3.1.0",
                 "hash-sum": "^1.0.2",
@@ -15636,12 +15678,12 @@
             }
         },
         "vue-template-compiler": {
-            "version": "2.6.14",
-            "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz",
-            "integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==",
+            "version": "2.7.3",
+            "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.3.tgz",
+            "integrity": "sha512-QKcV4vj9akZ2zSD1+F7tci3aXpRe5DXU3TZ/ZWJPE9gInXD5UoV+Q2PrCaplj4IGIK8JXRHYaSvOXkOn7tg9Og==",
             "requires": {
                 "de-indent": "^1.0.2",
-                "he": "^1.1.0"
+                "he": "^1.2.0"
             }
         },
         "vue-template-es2015-compiler": {
@@ -15802,9 +15844,9 @@
             }
         },
         "webpack-dev-server": {
-            "version": "4.9.2",
-            "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.2.tgz",
-            "integrity": "sha512-H95Ns95dP24ZsEzO6G9iT+PNw4Q7ltll1GfJHV4fKphuHWgKFzGHWi4alTlTnpk1SPPk41X+l2RB7rLfIhnB9Q==",
+            "version": "4.9.3",
+            "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz",
+            "integrity": "sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw==",
             "requires": {
                 "@types/bonjour": "^3.5.9",
                 "@types/connect-history-api-fallback": "^1.3.5",
@@ -15818,7 +15860,7 @@
                 "chokidar": "^3.5.3",
                 "colorette": "^2.0.10",
                 "compression": "^1.7.4",
-                "connect-history-api-fallback": "^1.6.0",
+                "connect-history-api-fallback": "^2.0.0",
                 "default-gateway": "^6.0.3",
                 "express": "^4.17.3",
                 "graceful-fs": "^4.2.6",

+ 1 - 1
package.json

@@ -13,7 +13,7 @@
         "pre-commit": "lint-staged"
     },
     "dependencies": {
-      "autoprefixer": "^10.4.1",
+        "autoprefixer": "^10.4.1",
         "axios": "^0.26.0",
         "cross-env": "^7.0.3",
         "dayjs": "^1.10.4",

+ 10 - 9
resources/js/components/WebauthnKeys.vue

@@ -1,17 +1,18 @@
 <template>
   <div>
     <div class="mt-6">
-      <h3 class="font-bold text-xl">Device Authentication (U2F)</h3>
+      <h3 class="font-bold text-xl">Device Authentication (WebAuthn)</h3>
 
       <div class="my-4 w-24 border-b-2 border-grey-200"></div>
 
       <p class="my-6">
-        Webauthn Keys you have registered for 2nd factor authentication. To remove a key simply
-        click the delete button next to it. Disabling all keys will turn off 2FA on your account.
+        Hardware security keys you have registered for 2nd factor authentication. To remove a key
+        simply click the delete button next to it. Disabling all keys will turn off 2FA on your
+        account.
       </p>
 
       <div>
-        <p class="mb-0" v-if="keys.length === 0">You have not registered any Webauthn Keys.</p>
+        <p class="mb-0" v-if="keys.length === 0">You have not registered any hardware keys.</p>
 
         <div class="table w-full text-sm md:text-base" v-if="keys.length > 0">
           <div class="table-row">
@@ -19,7 +20,7 @@
             <div class="table-cell p-1 md:p-4 font-semibold">Created</div>
             <div class="table-cell p-1 md:p-4 font-semibold">Enabled</div>
             <div class="table-cell p-1 md:p-4 text-right">
-              <a href="/webauthn/keys/create" class="text-indigo-700">Add New Device</a>
+              <a href="/webauthn/keys/create" class="text-indigo-700">Add New Key</a>
             </div>
           </div>
           <div v-for="key in keys" :key="key.id" class="table-row even:bg-grey-50 odd:bg-white">
@@ -46,15 +47,15 @@
         <h2
           class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
         >
-          Remove U2F Device
+          Remove Hardware Key
         </h2>
         <p v-if="keys.length === 1" class="my-4 text-grey-700">
-          Once this device is removed, <b>Two-Factor Authentication</b> will be disabled on your
+          Once this key is removed, <b>Two-Factor Authentication</b> will be disabled on your
           account.
         </p>
         <p v-else class="my-4 text-grey-700">
-          Once this device is removed, <b>Two-Factor Authentication</b> will still be enabled as you
-          have other U2F devices associated with your account.
+          Once this key is removed, <b>Two-Factor Authentication</b> will still be enabled as you
+          have other hardware keys associated with your account.
         </p>
         <div class="mt-6">
           <button

+ 10 - 0
resources/js/webauthn.js

@@ -236,4 +236,14 @@ WebAuthn.prototype.setNotify = function (callback) {
   this._notifyCallback = callback
 }
 
+!(function (e, t) {
+  'object' == typeof exports && 'object' == typeof module
+    ? (module.exports = t)
+    : 'function' == typeof define && define.amd
+    ? define([], t)
+    : 'object' == typeof exports
+    ? (exports.WebAuthn = t)
+    : (e.WebAuthn = t)
+})(this, WebAuthn)
+
 window.WebAuthn = WebAuthn

+ 1 - 1
resources/views/layouts/app.blade.php

@@ -33,7 +33,7 @@
 
     <footer>
       <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 lg:max-w-7xl">
-        <div class="border-t border-grey-200 py-4 text-sm text-grey-500 text-center"><a href="https://github.com/anonaddy/anonaddy/releases/tag/v{{ PragmaRX\Version\Package\Facade::version() }}" target="_blank" rel="nofollow noreferrer noopener" class="block sm:inline">v{{ PragmaRX\Version\Package\Facade::version() }}</a></div>
+        <div class="border-t border-grey-200 py-4 text-sm text-grey-500 text-center"><a href="https://github.com/anonaddy/anonaddy/releases/tag/v{{ App\Helpers\GitVersionHelper::version() }}" target="_blank" rel="nofollow noreferrer noopener" class="block sm:inline">v{{ App\Helpers\GitVersionHelper::version() }}</a></div>
       </div>
     </footer>
 

+ 4 - 4
resources/views/settings/show.blade.php

@@ -392,7 +392,7 @@
                 <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
 
                 <p class="mt-6">
-                    Two-factor authentication, also known as 2FA or multi-factor, adds an extra layer of security to your account beyond your username and password. There are <b>two options for 2FA</b> - Authentication App (e.g. Google Authenticator or another, Aegis, andOTP) or U2F Device Authentication (e.g. YubiKey, SoloKey, Nitrokey).
+                    Two-factor authentication, also known as 2FA or multi-factor, adds an extra layer of security to your account beyond your username and password. There are <b>two options for 2FA</b> - Authentication App (e.g. Google Authenticator or another, Aegis, andOTP) or Hardware Security Key (e.g. YubiKey, SoloKey, Nitrokey).
                 </p>
 
                 <p class="mt-4 pb-16">
@@ -516,19 +516,19 @@
                         <div class="pt-16">
 
                             <h3 class="font-bold text-xl">
-                                Enable Device Authentication (U2F)
+                                Enable Device Authentication (WebAuthn)
                             </h3>
 
                             <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
 
-                            <p class="my-6">U2F is a standard for universal two-factor authentication tokens. You can use any U2F key such as a Yubikey, Solokey, NitroKey etc.</p>
+                            <p class="my-6">WebAuthn is a new W3C global standard for secure authentication. You can use any hardware key such as a Yubikey, Solokey, NitroKey etc.</p>
 
                             <a
                             type="button"
                             href="{{ route('webauthn.create') }}"
                             class="block bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none text-center"
                             >
-                                Register U2F Device
+                                Register New Hardware Key
                             </a>
 
                         </div>

+ 25 - 5
resources/views/vendor/webauthn/authenticate.blade.php

@@ -37,7 +37,14 @@
 
                     <form method="POST" onsubmit="authenticateDevice();return false" action="{{ route('webauthn.auth') }}" id="form">
                         @csrf
-                        <input type="hidden" name="data" id="data" />
+                        <input type="hidden" name="id" id="id">
+                        <input type="hidden" name="rawId" id="rawId">
+                        <input type="hidden" name="response[authenticatorData]" id="authenticatorData">
+                        <input type="hidden" name="response[clientDataJSON]" id="clientDataJSON">
+                        <input type="hidden" name="response[signature]" id="signature">
+                        <input type="hidden" name="response[userHandle]" id="userHandle">
+                        <input type="hidden" name="type" id="type">
+
                     </form>
 
                     <div class="mt-4">
@@ -102,20 +109,33 @@
         if (! /apple/i.test(navigator.vendor)) {
             webauthn.sign(
                 publicKey,
-                function (datas) {
+                function (data) {
                     document.getElementById("success").classList.remove("hidden");
-                    document.getElementById("data").value = JSON.stringify(datas);
+                    document.getElementById("id").value = data.id;
+                    document.getElementById("rawId").value = data.rawId;
+                    document.getElementById("authenticatorData").value = data.response.authenticatorData;
+                    document.getElementById("clientDataJSON").value = data.response.clientDataJSON;
+                    document.getElementById("signature").value = data.response.signature;
+                    document.getElementById("userHandle").value = data.response.userHandle;
+                    document.getElementById("type").value = data.type;
                     document.getElementById("form").submit();
                 }
             );
         }
 
         function authenticateDevice() {
+            document.getElementById("error").classList.add("hidden");
             webauthn.sign(
                 publicKey,
-                function (datas) {
+                function (data) {
                     document.getElementById("success").classList.remove("hidden");
-                    document.getElementById("data").value = JSON.stringify(datas);
+                    document.getElementById("id").value = data.id;
+                    document.getElementById("rawId").value = data.rawId;
+                    document.getElementById("authenticatorData").value = data.response.authenticatorData;
+                    document.getElementById("clientDataJSON").value = data.response.clientDataJSON;
+                    document.getElementById("signature").value = data.response.signature;
+                    document.getElementById("userHandle").value = data.response.userHandle;
+                    document.getElementById("type").value = data.type;
                     document.getElementById("form").submit();
                 }
             );

+ 12 - 4
resources/views/vendor/webauthn/register.blade.php

@@ -33,9 +33,13 @@
                         {{ trans('webauthn::messages.noButtonAdvise') }}
                     </p>
 
-                    <form method="POST" onsubmit="registerDevice();return false" class="mt-8" action="{{ route('webauthn.store') }}" id="form">
+                    <form method="POST" onsubmit="registerDevice();return false"  class="mt-8" action="{{ route('webauthn.store') }}" id="form">
                         @csrf
-                        <input type="hidden" name="register" id="register">
+                        <input type="hidden" name="id" id="id">
+                        <input type="hidden" name="rawId" id="rawId">
+                        <input type="hidden" name="response[attestationObject]" id="attestationObject">
+                        <input type="hidden" name="response[clientDataJSON]" id="clientDataJSON">
+                        <input type="hidden" name="type" id="type">
 
                         <label for="name" class="block text-grey-700 text-sm mb-2">
                             Name:
@@ -119,9 +123,13 @@
 
             webauthn.register(
                 publicKey,
-                function (datas) {
+                function (data) {
                     document.getElementById("success").classList.remove("hidden");
-                    document.getElementById("register").value = JSON.stringify(datas);
+                    document.getElementById("id").value = data.id;
+                    document.getElementById("rawId").value = data.rawId;
+                    document.getElementById("attestationObject").value = data.response.attestationObject;
+                    document.getElementById("clientDataJSON").value = data.response.clientDataJSON;
+                    document.getElementById("type").value = data.type;
                     document.getElementById("form").submit();
                 }
             );

+ 127 - 72
routes/api.php

@@ -1,5 +1,27 @@
 <?php
 
+use App\Http\Controllers\Api\AccountDetailController;
+use App\Http\Controllers\Api\ActiveAliasController;
+use App\Http\Controllers\Api\ActiveDomainController;
+use App\Http\Controllers\Api\ActiveRuleController;
+use App\Http\Controllers\Api\ActiveUsernameController;
+use App\Http\Controllers\Api\AliasController;
+use App\Http\Controllers\Api\AliasRecipientController;
+use App\Http\Controllers\Api\AllowedRecipientController;
+use App\Http\Controllers\Api\AppVersionController;
+use App\Http\Controllers\Api\CatchAllDomainController;
+use App\Http\Controllers\Api\CatchAllUsernameController;
+use App\Http\Controllers\Api\DomainController;
+use App\Http\Controllers\Api\DomainDefaultRecipientController;
+use App\Http\Controllers\Api\DomainOptionController;
+use App\Http\Controllers\Api\EncryptedRecipientController;
+use App\Http\Controllers\Api\FailedDeliveryController;
+use App\Http\Controllers\Api\RecipientController;
+use App\Http\Controllers\Api\RecipientKeyController;
+use App\Http\Controllers\Api\ReorderRuleController;
+use App\Http\Controllers\Api\RuleController;
+use App\Http\Controllers\Api\UsernameController;
+use App\Http\Controllers\Api\UsernameDefaultRecipientController;
 use Illuminate\Support\Facades\Route;
 
 /*
@@ -17,76 +39,109 @@ Route::group([
   'middleware' => ['auth:api', 'verified'],
   'prefix' => 'v1'
 ], function () {
-    Route::get('/aliases', 'Api\AliasController@index');
-    Route::get('/aliases/{id}', 'Api\AliasController@show');
-    Route::post('/aliases', 'Api\AliasController@store');
-    Route::patch('/aliases/{id}', 'Api\AliasController@update');
-    Route::patch('/aliases/{id}/restore', 'Api\AliasController@restore');
-    Route::delete('/aliases/{id}', 'Api\AliasController@destroy');
-    Route::delete('/aliases/{id}/forget', 'Api\AliasController@forget');
-
-    Route::post('/active-aliases', 'Api\ActiveAliasController@store');
-    Route::delete('/active-aliases/{id}', 'Api\ActiveAliasController@destroy');
-
-    Route::post('/alias-recipients', 'Api\AliasRecipientController@store');
-
-    Route::get('/recipients', 'Api\RecipientController@index');
-    Route::get('/recipients/{id}', 'Api\RecipientController@show');
-    Route::post('/recipients', 'Api\RecipientController@store');
-    Route::delete('/recipients/{id}', 'Api\RecipientController@destroy');
-
-    Route::patch('/recipient-keys/{id}', 'Api\RecipientKeyController@update');
-    Route::delete('/recipient-keys/{id}', 'Api\RecipientKeyController@destroy');
-
-    Route::post('/encrypted-recipients', 'Api\EncryptedRecipientController@store');
-    Route::delete('/encrypted-recipients/{id}', 'Api\EncryptedRecipientController@destroy');
-
-    Route::post('/allowed-recipients', 'Api\AllowedRecipientController@store');
-    Route::delete('/allowed-recipients/{id}', 'Api\AllowedRecipientController@destroy');
-
-    Route::get('/domains', 'Api\DomainController@index');
-    Route::get('/domains/{id}', 'Api\DomainController@show');
-    Route::post('/domains', 'Api\DomainController@store');
-    Route::patch('/domains/{id}', 'Api\DomainController@update');
-    Route::delete('/domains/{id}', 'Api\DomainController@destroy');
-    Route::patch('/domains/{id}/default-recipient', 'Api\DomainDefaultRecipientController@update');
-
-    Route::post('/active-domains', 'Api\ActiveDomainController@store');
-    Route::delete('/active-domains/{id}', 'Api\ActiveDomainController@destroy');
-
-    Route::post('/catch-all-domains', 'Api\CatchAllDomainController@store');
-    Route::delete('/catch-all-domains/{id}', 'Api\CatchAllDomainController@destroy');
-
-    Route::get('/usernames', 'Api\UsernameController@index');
-    Route::get('/usernames/{id}', 'Api\UsernameController@show');
-    Route::post('/usernames', 'Api\UsernameController@store');
-    Route::patch('/usernames/{id}', 'Api\UsernameController@update');
-    Route::delete('/usernames/{id}', 'Api\UsernameController@destroy');
-    Route::patch('/usernames/{id}/default-recipient', 'Api\UsernameDefaultRecipientController@update');
-
-    Route::post('/active-usernames', 'Api\ActiveUsernameController@store');
-    Route::delete('/active-usernames/{id}', 'Api\ActiveUsernameController@destroy');
-
-    Route::post('/catch-all-usernames', 'Api\CatchAllUsernameController@store');
-    Route::delete('/catch-all-usernames/{id}', 'Api\CatchAllUsernameController@destroy');
-
-    Route::get('/rules', 'Api\RuleController@index');
-    Route::get('/rules/{id}', 'Api\RuleController@show');
-    Route::post('/rules', 'Api\RuleController@store');
-    Route::patch('/rules/{id}', 'Api\RuleController@update');
-    Route::delete('/rules/{id}', 'Api\RuleController@destroy');
-    Route::post('/reorder-rules', 'Api\ReorderRuleController@store');
-
-    Route::post('/active-rules', 'Api\ActiveRuleController@store');
-    Route::delete('/active-rules/{id}', 'Api\ActiveRuleController@destroy');
-
-    Route::get('/failed-deliveries', 'Api\FailedDeliveryController@index');
-    Route::get('/failed-deliveries/{id}', 'Api\FailedDeliveryController@show');
-    Route::delete('/failed-deliveries/{id}', 'Api\FailedDeliveryController@destroy');
-
-    Route::get('/domain-options', 'Api\DomainOptionController@index');
-
-    Route::get('/account-details', 'Api\AccountDetailController@index');
-
-    Route::get('/app-version', 'Api\AppVersionController@index');
+    Route::controller(AliasController::class)->group(function () {
+        Route::get('/aliases', 'index');
+        Route::get('/aliases/{id}', 'show');
+        Route::post('/aliases', 'store');
+        Route::patch('/aliases/{id}', 'update');
+        Route::patch('/aliases/{id}/restore', 'restore');
+        Route::delete('/aliases/{id}', 'destroy');
+        Route::delete('/aliases/{id}/forget', 'forget');
+    });
+
+    Route::controller(ActiveAliasController::class)->group(function () {
+        Route::post('/active-aliases', 'store');
+        Route::delete('/active-aliases/{id}', 'destroy');
+    });
+
+    Route::post('/alias-recipients', [AliasRecipientController::class, 'store']);
+
+    Route::controller(RecipientController::class)->group(function () {
+        Route::get('/recipients', 'index');
+        Route::get('/recipients/{id}', 'show');
+        Route::post('/recipients', 'store');
+        Route::delete('/recipients/{id}', 'destroy');
+    });
+
+    Route::controller(RecipientKeyController::class)->group(function () {
+        Route::patch('/recipient-keys/{id}', 'update');
+        Route::delete('/recipient-keys/{id}', 'destroy');
+    });
+
+    Route::controller(EncryptedRecipientController::class)->group(function () {
+        Route::post('/encrypted-recipients', 'store');
+        Route::delete('/encrypted-recipients/{id}', 'destroy');
+    });
+
+    Route::controller(AllowedRecipientController::class)->group(function () {
+        Route::post('/allowed-recipients', 'store');
+        Route::delete('/allowed-recipients/{id}', 'destroy');
+    });
+
+    Route::controller(DomainController::class)->group(function () {
+        Route::get('/domains', 'index');
+        Route::get('/domains/{id}', 'show');
+        Route::post('/domains', 'store');
+        Route::patch('/domains/{id}', 'update');
+        Route::delete('/domains/{id}', 'destroy');
+    });
+
+    Route::patch('/domains/{id}/default-recipient', [DomainDefaultRecipientController::class, 'update']);
+
+    Route::controller(ActiveDomainController::class)->group(function () {
+        Route::post('/active-domains', 'store');
+        Route::delete('/active-domains/{id}', 'destroy');
+    });
+
+    Route::controller(CatchAllDomainController::class)->group(function () {
+        Route::post('/catch-all-domains', 'store');
+        Route::delete('/catch-all-domains/{id}', 'destroy');
+    });
+
+    Route::controller(UsernameController::class)->group(function () {
+        Route::get('/usernames', 'index');
+        Route::get('/usernames/{id}', 'show');
+        Route::post('/usernames', 'store');
+        Route::patch('/usernames/{id}', 'update');
+        Route::delete('/usernames/{id}', 'destroy');
+    });
+
+    Route::patch('/usernames/{id}/default-recipient', [UsernameDefaultRecipientController::class, 'update']);
+
+    Route::controller(ActiveUsernameController::class)->group(function () {
+        Route::post('/active-usernames', 'store');
+        Route::delete('/active-usernames/{id}', 'destroy');
+    });
+
+    Route::controller(CatchAllUsernameController::class)->group(function () {
+        Route::post('/catch-all-usernames', 'store');
+        Route::delete('/catch-all-usernames/{id}', 'destroy');
+    });
+
+    Route::controller(RuleController::class)->group(function () {
+        Route::get('/rules', 'index');
+        Route::get('/rules/{id}', 'show');
+        Route::post('/rules', 'store');
+        Route::patch('/rules/{id}', 'update');
+        Route::delete('/rules/{id}', 'destroy');
+    });
+
+    Route::post('/reorder-rules', [ReorderRuleController::class, 'store']);
+
+    Route::controller(ActiveRuleController::class)->group(function () {
+        Route::post('/active-rules', 'store');
+        Route::delete('/active-rules/{id}', 'destroy');
+    });
+
+    Route::controller(FailedDeliveryController::class)->group(function () {
+        Route::get('/failed-deliveries', 'index');
+        Route::get('/failed-deliveries/{id}', 'show');
+        Route::delete('/failed-deliveries/{id}', 'destroy');
+    });
+
+    Route::get('/domain-options', [DomainOptionController::class, 'index']);
+
+    Route::get('/account-details', [AccountDetailController::class, 'index']);
+
+    Route::get('/app-version', [AppVersionController::class, 'index']);
 });

+ 79 - 37
routes/web.php

@@ -1,5 +1,30 @@
 <?php
 
+use App\Http\Controllers\AliasExportController;
+use App\Http\Controllers\Auth\BackupCodeController;
+use App\Http\Controllers\Auth\ForgotUsernameController;
+use App\Http\Controllers\Auth\TwoFactorAuthController;
+use App\Http\Controllers\Auth\WebauthnController;
+use App\Http\Controllers\Auth\WebauthnEnabledKeyController;
+use App\Http\Controllers\BannerLocationController;
+use App\Http\Controllers\BrowserSessionController;
+use App\Http\Controllers\DeactivateAliasController;
+use App\Http\Controllers\DefaultAliasDomainController;
+use App\Http\Controllers\DefaultAliasFormatController;
+use App\Http\Controllers\DefaultRecipientController;
+use App\Http\Controllers\DomainVerificationController;
+use App\Http\Controllers\EmailSubjectController;
+use App\Http\Controllers\FromNameController;
+use App\Http\Controllers\PasswordController;
+use App\Http\Controllers\RecipientVerificationController;
+use App\Http\Controllers\SettingController;
+use App\Http\Controllers\ShowAliasController;
+use App\Http\Controllers\ShowDomainController;
+use App\Http\Controllers\ShowFailedDeliveryController;
+use App\Http\Controllers\ShowRecipientController;
+use App\Http\Controllers\ShowRuleController;
+use App\Http\Controllers\ShowUsernameController;
+use App\Http\Controllers\UseReplyToController;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Route;
 
@@ -15,43 +40,54 @@ use Illuminate\Support\Facades\Route;
 */
 
 Auth::routes(['verify' => true, 'register' => config('anonaddy.enable_registration')]);
-Route::get('/username/reminder', 'Auth\ForgotUsernameController@show')->name('username.reminder.show');
-Route::post('/username/email', 'Auth\ForgotUsernameController@sendReminderEmail')->name('username.email');
 
-Route::post('/login/2fa', 'Auth\TwoFactorAuthController@authenticateTwoFactor')->name('login.2fa')->middleware(['2fa', 'throttle', 'auth']);
 
-Route::get('/login/backup-code', 'Auth\BackupCodeController@index')->name('login.backup_code.index');
-Route::post('/login/backup-code', 'Auth\BackupCodeController@login')->name('login.backup_code.login');
+Route::controller(ForgotUsernameController::class)->group(function () {
+    Route::get('/username/reminder', 'show')->name('username.reminder.show');
+    Route::post('/username/email', 'sendReminderEmail')->name('username.email');
+});
+
+Route::post('/login/2fa', [TwoFactorAuthController::class, 'authenticateTwoFactor'])->name('login.2fa')->middleware(['2fa', 'throttle', 'auth']);
+
+Route::controller(BackupCodeController::class)->group(function () {
+    Route::get('/login/backup-code', 'index')->name('login.backup_code.index');
+    Route::post('/login/backup-code', 'login')->name('login.backup_code.login');
+});
 
 Route::group([
     'middleware' => config('webauthn.middleware', []),
     'domain' => config('webauthn.domain', null),
     'prefix' => config('webauthn.prefix', 'webauthn'),
 ], function () {
-    Route::get('keys', 'Auth\WebauthnController@index')->name('webauthn.index');
-    Route::get('keys/create', 'Auth\WebauthnController@create')->name('webauthn.create');
-    Route::post('keys', 'Auth\WebauthnController@store')->name('webauthn.store');
-    Route::delete('keys/{id}', 'Auth\WebauthnController@destroy')->name('webauthn.destroy');
-    Route::post('enabled-keys', 'Auth\WebauthnEnabledKeyController@store')->name('webauthn.enabled_key.store');
-    Route::delete('enabled-keys/{id}', 'Auth\WebauthnEnabledKeyController@destroy')->name('webauthn.enabled_key.destroy');
+    Route::controller(WebauthnController::class)->group(function () {
+        Route::get('keys', 'index')->name('webauthn.index');
+        Route::get('keys/create', 'create')->name('webauthn.create');
+        Route::post('keys', 'store')->name('webauthn.store');
+        Route::delete('keys/{id}', 'destroy')->name('webauthn.destroy');
+    });
+
+    Route::controller(WebauthnEnabledKeyController::class)->group(function () {
+        Route::post('enabled-keys', 'store')->name('webauthn.enabled_key.store');
+        Route::delete('enabled-keys/{id}', 'destroy')->name('webauthn.enabled_key.destroy');
+    });
 });
 
 Route::middleware(['auth', 'verified', '2fa', 'webauthn'])->group(function () {
-    Route::get('/', 'ShowAliasController@index')->name('aliases.index');
+    Route::get('/', [ShowAliasController::class, 'index'])->name('aliases.index');
 
-    Route::get('/recipients', 'ShowRecipientController@index')->name('recipients.index');
-    Route::post('/recipients/email/resend', 'RecipientVerificationController@resend');
+    Route::get('/recipients', [ShowRecipientController::class, 'index'])->name('recipients.index');
+    Route::post('/recipients/email/resend', [RecipientVerificationController::class, 'resend']);
 
-    Route::get('/domains', 'ShowDomainController@index')->name('domains.index');
-    Route::get('/domains/{id}/check-sending', 'DomainVerificationController@checkSending');
+    Route::get('/domains', [ShowDomainController::class, 'index'])->name('domains.index');
+    Route::get('/domains/{id}/check-sending', [DomainVerificationController::class, 'checkSending']);
 
-    Route::get('/usernames', 'ShowUsernameController@index')->name('usernames.index');
+    Route::get('/usernames', [ShowUsernameController::class, 'index'])->name('usernames.index');
 
-    Route::get('/deactivate/{alias}', 'DeactivateAliasController@deactivate')->name('deactivate');
+    Route::get('/deactivate/{alias}', [DeactivateAliasController::class, 'deactivate'])->name('deactivate');
 
-    Route::get('/rules', 'ShowRuleController@index')->name('rules.index');
+    Route::get('/rules', [ShowRuleController::class, 'index'])->name('rules.index');
 
-    Route::get('/failed-deliveries', 'ShowFailedDeliveryController@index')->name('failed_deliveries.index');
+    Route::get('/failed-deliveries', [ShowFailedDeliveryController::class, 'index'])->name('failed_deliveries.index');
 });
 
 
@@ -59,33 +95,39 @@ Route::group([
     'middleware' => ['auth', '2fa', 'webauthn'],
     'prefix' => 'settings'
 ], function () {
-    Route::get('/', 'SettingController@show')->name('settings.show');
-    Route::post('/account', 'SettingController@destroy')->name('account.destroy');
+    Route::controller(SettingController::class)->group(function () {
+        Route::get('/', 'show')->name('settings.show');
+        Route::post('/account', 'destroy')->name('account.destroy');
+    });
 
-    Route::post('/default-recipient', 'DefaultRecipientController@update')->name('settings.default_recipient');
-    Route::post('/edit-default-recipient', 'DefaultRecipientController@edit')->name('settings.edit_default_recipient');
+    Route::controller(DefaultRecipientController::class)->group(function () {
+        Route::post('/default-recipient', 'update')->name('settings.default_recipient');
+        Route::post('/edit-default-recipient', 'edit')->name('settings.edit_default_recipient');
+    });
 
-    Route::post('/default-alias-domain', 'DefaultAliasDomainController@update')->name('settings.default_alias_domain');
+    Route::post('/default-alias-domain', [DefaultAliasDomainController::class, 'update'])->name('settings.default_alias_domain');
 
-    Route::post('/default-alias-format', 'DefaultAliasFormatController@update')->name('settings.default_alias_format');
+    Route::post('/default-alias-format', [DefaultAliasFormatController::class, 'update'])->name('settings.default_alias_format');
 
-    Route::post('/from-name', 'FromNameController@update')->name('settings.from_name');
+    Route::post('/from-name', [FromNameController::class, 'update'])->name('settings.from_name');
 
-    Route::post('/email-subject', 'EmailSubjectController@update')->name('settings.email_subject');
+    Route::post('/email-subject', [EmailSubjectController::class, 'update'])->name('settings.email_subject');
 
-    Route::post('/banner-location', 'BannerLocationController@update')->name('settings.banner_location');
+    Route::post('/banner-location', [BannerLocationController::class, 'update'])->name('settings.banner_location');
 
-    Route::post('/use-reply-to', 'UseReplyToController@update')->name('settings.use_reply_to');
+    Route::post('/use-reply-to', [UseReplyToController::class, 'update'])->name('settings.use_reply_to');
 
-    Route::post('/password', 'PasswordController@update')->name('settings.password');
+    Route::post('/password', [PasswordController::class, 'update'])->name('settings.password');
 
-    Route::delete('/browser-sessions', 'BrowserSessionController@destroy')->name('browser-sessions.destroy');
+    Route::delete('/browser-sessions', [BrowserSessionController::class, 'destroy'])->name('browser-sessions.destroy');
 
-    Route::post('/2fa/enable', 'Auth\TwoFactorAuthController@store')->name('settings.2fa_enable');
-    Route::post('/2fa/regenerate', 'Auth\TwoFactorAuthController@update')->name('settings.2fa_regenerate');
-    Route::post('/2fa/disable', 'Auth\TwoFactorAuthController@destroy')->name('settings.2fa_disable');
+    Route::controller(TwoFactorAuthController::class)->group(function () {
+        Route::post('/2fa/enable', 'store')->name('settings.2fa_enable');
+        Route::post('/2fa/regenerate', 'update')->name('settings.2fa_regenerate');
+        Route::post('/2fa/disable', 'destroy')->name('settings.2fa_disable');
+    });
 
-    Route::post('/2fa/new-backup-code', 'Auth\BackupCodeController@update')->name('settings.new_backup_code');
+    Route::post('/2fa/new-backup-code', [BackupCodeController::class, 'update'])->name('settings.new_backup_code');
 
-    Route::get('/aliases/export', 'AliasExportController@export')->name('aliases.export');
+    Route::get('/aliases/export', [AliasExportController::class, 'export'])->name('aliases.export');
 });

+ 1 - 1
tests/Feature/Api/AppVersionTest.php

@@ -2,8 +2,8 @@
 
 namespace Tests\Feature\Api;
 
+use App\Helpers\GitVersionHelper as Version;
 use Illuminate\Foundation\Testing\RefreshDatabase;
-use PragmaRX\Version\Package\Facade as Version;
 use Tests\TestCase;
 
 class AppVersionTest extends TestCase

+ 1 - 1
tests/Feature/Api/RulesTest.php

@@ -449,7 +449,7 @@ class RulesTest extends TestCase
 
     protected function getParser($file)
     {
-        $parser = new Parser;
+        $parser = new Parser();
 
         // Fix some edge cases in from name e.g. "\" John Doe \"" <johndoe@example.com>
         $parser->addMiddleware(function ($mimePart, $next) {

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini