Upgrade to Laravel 9

This commit is contained in:
Will Browning
2022-07-07 08:44:47 +01:00
parent 831fc862d0
commit 43e094ac93
92 changed files with 3001 additions and 3272 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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.

View File

@@ -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

View File

@@ -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()
],
]);

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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 (! empty($config['encryption'])) {
$transport->setEncryption($config['encryption']);
if ($config === null) {
throw new InvalidArgumentException("Mailer [{$name}] is not defined.");
}
// 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']);
// 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']
);
$transport->setPassword($config['password']);
if ($this->app->bound('queue')) {
$mailer->setQueue($this->app['queue']);
}
return $this->configureSmtpTransport($transport, $config);
// 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 $mailer;
}
}

View File

@@ -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 {
//
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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']));
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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)");
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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']));
}
}

View File

@@ -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');
}
}

View File

@@ -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]
]);
}
}

View File

@@ -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'],
], [

View File

@@ -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());
$publicKey = app(PrepareCreationData::class)($request->user());
$request->session()->put(Webauthn::SESSION_PUBLICKEY_CREATION, $publicKey);
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([

View File

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

View File

@@ -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!']);
}

View File

@@ -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,

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -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';

View File

@@ -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()
]
];
}

View File

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

View File

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

View File

@@ -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()
],
];
}

View File

@@ -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;

View File

@@ -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();
$part->setContentId(base64_decode($attachment['contentId']));
$part->setFileName(base64_decode($attachment['file_name']));
$message->attachPart($part);
}
$message->getHeaders()
->addTextHeader('X-Old-Cids', implode(',', $cids));
$message->getHeaders()
->addTextHeader('X-New-Cids', implode(',', $newCids));
}
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;
}
}

View File

@@ -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();
$part->setContentId(base64_decode($attachment['contentId']));
$part->setFileName(base64_decode($attachment['file_name']));
$message->attachPart($part);
}
$message->getHeaders()
->addTextHeader('X-Old-Cids', implode(',', $cids));
$message->getHeaders()
->addTextHeader('X-New-Cids', implode(',', $newCids));
}
});
@@ -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;
}
}

View File

@@ -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();
$part->setContentId(base64_decode($attachment['contentId']));
$part->setFileName(base64_decode($attachment['file_name']));
$message->attachPart($part);
}
$message->getHeaders()
->addTextHeader('X-Old-Cids', implode(',', $cids));
$message->getHeaders()
->addTextHeader('X-New-Cids', implode(',', $newCids));
}
});
@@ -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;
}
}

View File

@@ -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');
});
}
}

View File

@@ -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;

View File

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

View File

@@ -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;

View File

@@ -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'));

View File

@@ -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;

View File

@@ -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());
}
/**

View File

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

View File

@@ -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';

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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);
}
});
}

View File

@@ -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);
}
});
}

View File

@@ -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);
}
});
}

View File

@@ -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);
}
});
}

View File

@@ -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);
}
});
}

View File

@@ -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');
});
}
/**

View File

@@ -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);
}
});
}

View File

@@ -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);
}
});
}

View File

@@ -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);
}
});
}

View File

@@ -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;

View File

@@ -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();
});
}

View File

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

View File

@@ -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');
}
/**

View File

@@ -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());
});
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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');\""

2529
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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(),
];

View File

@@ -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' => [

View File

@@ -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.
|
*/

View File

@@ -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),

View File

@@ -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,
],
],

View File

@@ -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'),
],
],
];

View File

@@ -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.
|
| 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.
|
| Supported: "smtp", "sendmail", "mailgun", "ses",
| "postmark", "log", "array", "failover"
|
*/
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'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),
],
/*
|--------------------------------------------------------------------------
| SMTP Host Port
|--------------------------------------------------------------------------
|
| 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.
|
*/
'ses' => [
'transport' => 'ses',
],
'port' => env('MAIL_PORT', 587),
'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'),
];

View File

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

View File

@@ -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}'

View File

@@ -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',
/*
|--------------------------------------------------------------------------
| Prefix path
| Username / Email
|--------------------------------------------------------------------------
|
| The uri prefix for all webauthn requests.
| 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',
/*
|--------------------------------------------------------------------------
| Webauthn Routes Prefix / Subdomain
|--------------------------------------------------------------------------
|
| 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,
];

View File

@@ -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();
});
}
};

262
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
);

View File

@@ -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();
}
);

View File

@@ -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::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::post('/active-aliases', 'Api\ActiveAliasController@store');
Route::delete('/active-aliases/{id}', 'Api\ActiveAliasController@destroy');
Route::controller(ActiveAliasController::class)->group(function () {
Route::post('/active-aliases', 'store');
Route::delete('/active-aliases/{id}', 'destroy');
});
Route::post('/alias-recipients', 'Api\AliasRecipientController@store');
Route::post('/alias-recipients', [AliasRecipientController::class, '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::controller(RecipientController::class)->group(function () {
Route::get('/recipients', 'index');
Route::get('/recipients/{id}', 'show');
Route::post('/recipients', 'store');
Route::delete('/recipients/{id}', 'destroy');
});
Route::patch('/recipient-keys/{id}', 'Api\RecipientKeyController@update');
Route::delete('/recipient-keys/{id}', 'Api\RecipientKeyController@destroy');
Route::controller(RecipientKeyController::class)->group(function () {
Route::patch('/recipient-keys/{id}', 'update');
Route::delete('/recipient-keys/{id}', 'destroy');
});
Route::post('/encrypted-recipients', 'Api\EncryptedRecipientController@store');
Route::delete('/encrypted-recipients/{id}', 'Api\EncryptedRecipientController@destroy');
Route::controller(EncryptedRecipientController::class)->group(function () {
Route::post('/encrypted-recipients', 'store');
Route::delete('/encrypted-recipients/{id}', 'destroy');
});
Route::post('/allowed-recipients', 'Api\AllowedRecipientController@store');
Route::delete('/allowed-recipients/{id}', 'Api\AllowedRecipientController@destroy');
Route::controller(AllowedRecipientController::class)->group(function () {
Route::post('/allowed-recipients', 'store');
Route::delete('/allowed-recipients/{id}', '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::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::post('/active-domains', 'Api\ActiveDomainController@store');
Route::delete('/active-domains/{id}', 'Api\ActiveDomainController@destroy');
Route::patch('/domains/{id}/default-recipient', [DomainDefaultRecipientController::class, 'update']);
Route::post('/catch-all-domains', 'Api\CatchAllDomainController@store');
Route::delete('/catch-all-domains/{id}', 'Api\CatchAllDomainController@destroy');
Route::controller(ActiveDomainController::class)->group(function () {
Route::post('/active-domains', 'store');
Route::delete('/active-domains/{id}', '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::controller(CatchAllDomainController::class)->group(function () {
Route::post('/catch-all-domains', 'store');
Route::delete('/catch-all-domains/{id}', 'destroy');
});
Route::post('/active-usernames', 'Api\ActiveUsernameController@store');
Route::delete('/active-usernames/{id}', 'Api\ActiveUsernameController@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::post('/catch-all-usernames', 'Api\CatchAllUsernameController@store');
Route::delete('/catch-all-usernames/{id}', 'Api\CatchAllUsernameController@destroy');
Route::patch('/usernames/{id}/default-recipient', [UsernameDefaultRecipientController::class, 'update']);
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::controller(ActiveUsernameController::class)->group(function () {
Route::post('/active-usernames', 'store');
Route::delete('/active-usernames/{id}', 'destroy');
});
Route::post('/active-rules', 'Api\ActiveRuleController@store');
Route::delete('/active-rules/{id}', 'Api\ActiveRuleController@destroy');
Route::controller(CatchAllUsernameController::class)->group(function () {
Route::post('/catch-all-usernames', 'store');
Route::delete('/catch-all-usernames/{id}', '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::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::get('/domain-options', 'Api\DomainOptionController@index');
Route::post('/reorder-rules', [ReorderRuleController::class, 'store']);
Route::get('/account-details', 'Api\AccountDetailController@index');
Route::controller(ActiveRuleController::class)->group(function () {
Route::post('/active-rules', 'store');
Route::delete('/active-rules/{id}', 'destroy');
});
Route::get('/app-version', 'Api\AppVersionController@index');
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']);
});

View File

@@ -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');
});

View File

@@ -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

View File

@@ -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) {