Compare commits

..

No commits in common. "master" and "v1.0.1" have entirely different histories.

218 changed files with 4979 additions and 8960 deletions

5
.gitignore vendored
View file

@ -1,4 +1,3 @@
/.phpunit.cache
/node_modules
/public/hot
/public/storage
@ -10,18 +9,14 @@
/storage/debugbar
/vendor
/postfix/vendor
/.fleet
/.idea
/.vscode
/.vagrant
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
.env
.env.backup
.env.production
.php-cs-fixer.cache
.phpunit.result.cache
ray.php

View file

@ -4,9 +4,9 @@ This is the source code for self-hosting addy.io.
## FAQ
- [Why is it called addy.io?](#why-is-it-called-addyio)
- [Why is it called addy.io?](#why-is-it-called-addy-io)
- [Why did you make this site?](#why-did-you-make-this-site)
- [Why should I use addy.io?](#why-should-i-use-addyio)
- [Why should I use addy.io?](#why-should-i-use-addy-io)
- [Do you store emails?](#do-you-store-emails)
- [What is a shared domain alias?](#what-is-a-shared-domain-alias)
- [What is a standard alias?](#what-is-a-standard-alias)
@ -21,11 +21,6 @@ This is the source code for self-hosting addy.io.
- [How do I add my own GPG/OpenPGP key for encryption?](#how-do-i-add-my-own-gpgopenpgp-key-for-encryption)
- [Are attachments encrypted too?](#are-attachments-encrypted-too)
- [Are forwarded emails signed when encryption is enabled?](#are-forwarded-emails-signed-when-encryption-is-enabled)
- [Can I reply/send from aliases using encryption?](#can-i-replysend-from-aliases-using-encryption)
- [Is my public GPG/OpenPGP key removed when I reply/send from an alias?](#is-my-public-gpgopenpgp-key-removed-when-i-replysend-from-an-alias)
- [Can I mark emails forwarded to me by addy.io as spam?](#can-i-mark-emails-forwarded-to-me-by-addyio-as-spam)
- [Can I use aliases to create multiple accounts on other websites and services?](#can-i-use-aliases-to-create-multiple-accounts-on-other-websites-and-services)
- [Can I have multiple Free accounts?](#can-i-have-multiple-free-accounts)
- [What if I don't want anyone to link ownership of my aliases together?](#what-if-i-dont-want-anyone-to-link-ownership-of-my-aliases-together)
- [Where is the server located?](#where-is-the-server-located)
- [What if I don't trust you?](#what-if-i-dont-trust-you)
@ -35,15 +30,13 @@ This is the source code for self-hosting addy.io.
- [How do I reply to a forwarded email?](#how-do-i-reply-to-a-forwarded-email)
- [I'm trying to reply/send from an alias but the email keeps coming back to me, what's wrong?](#im-trying-to-replysend-from-an-alias-but-the-email-keeps-coming-back-to-me-whats-wrong)
- [I'm trying to reply/send from an alias but it is rejected, what's wrong?](#im-trying-to-replysend-from-an-alias-but-it-is-rejected-whats-wrong)
- [I've been forwarded an email with a red warning banner saying it may have been spoofed, what does it mean?](#ive-been-forwarded-an-email-with-a-red-warning-banner-saying-it-may-have-been-spoofed-what-does-it-mean)
- [Does addy.io strip out the banner information when I reply to an email?](#does-addyio-strip-out-the-banner-information-when-i-reply-to-an-email)
- [Does addy.io strip out the banner information when I reply to an email?](#does-addy-io-strip-out-the-banner-information-when-i-reply-to-an-email)
- [How do I send email from an alias?](#how-do-i-send-email-from-an-alias)
- [Will people see my real email if I reply to a forwarded one?](#will-people-see-my-real-email-if-i-reply-to-a-forwarded-one)
- [Can emails have attachments?](#can-emails-have-attachments)
- [What is the max email size limit?](#what-is-the-max-email-size-limit)
- [What happens if I have a subscription but then cancel it?](#what-happens-if-i-have-a-subscription-but-then-cancel-it)
- [If I subscribe will Stripe see my real email address?](#if-i-subscribe-will-stripe-see-my-real-email-address)
- [Do you offer student discount?](#do-you-offer-student-discount)
- [How do you prevent spammers?](#how-do-you-prevent-spammers)
- [What do you use to do DNS lookups on domain names?](#what-do-you-use-to-do-dns-lookups-on-domain-names)
- [Is there a limit to how many emails I can forward?](#is-there-a-limit-to-how-many-emails-i-can-forward)
@ -55,10 +48,10 @@ This is the source code for self-hosting addy.io.
- [I'm not receiving any emails, what's wrong?](#im-not-receiving-any-emails-whats-wrong)
- [I'm having trouble logging in, what's wrong?](#im-having-trouble-logging-in-whats-wrong)
- [How do I know this site won't disappear next month?](#how-do-i-know-this-site-wont-disappear-next-month)
- [What happens to addy.io if you die?](#what-happens-to-addyio-if-you-die)
- [What happens to addy.io if you die?](#what-happens-to-addy-io-if-you-die)
- [Is the application tested?](#is-the-appliction-tested)
- [How do I host this myself?](#how-do-i-host-this-myself)
- [Who's behind addy.io?](#whos-behind-addyio)
- [Who's behind addy.io?](#whos-behind-addy-io)
- [I couldn't find an answer to my question, how can I contact you?](#i-couldnt-find-an-answer-to-my-question-how-can-i-contact-you)
## Why is it called addy.io?
@ -95,7 +88,7 @@ There are a number of reasons you should consider using this service:
## Do you store emails?
Emails are only ever stored in the event of a failed delivery, and only if you have this option enabled in your account settings.
No I definitely do not store/save any emails that pass through the server.
## What is a shared domain alias?
@ -180,32 +173,6 @@ You can add this key to your own keyring so that you can verify emails have come
The fingerprint of the no-reply@addy.io key is "26A987650243B28802524E2F809FD0D502E2F695" you can find the key on [https://keys.openpgp.org](https://keys.openpgp.org/search?q=26A987650243B28802524E2F809FD0D502E2F695).
## Can I reply/send from aliases using encryption?
1. If the person you are sending your message to **already uses GPG/OpenPGP encryption** then you can simply encrypt your reply/send from your alias using their public key.
2. If the person you are sending your message to **does not use GPG/OpenPGP encryption** then you can instead encrypt your reply/send with the `no-reply@addy.io` [public key](https://keys.openpgp.org/search?q=26A987650243B28802524E2F809FD0D502E2F695) (<span class="break-words">"26A987650243B28802524E2F809FD0D502E2F695"</span>). Your reply/send will then be **automatically decrypted** on the addy.io server before being sent on to the correct destination in clear text. This is useful if you wish to hide your replies/sends from your email provider such as Gmail.
## Is my public GPG/OpenPGP key removed when I reply/send from an alias?
Yes, any attached GPG/OpenPGP public keys or GPG/OpenPGP signatures are automatically removed when replying or sending from an alias. This is to prevent you accidentally revealing your real email address which is usually shown as an identity in your public key.
## Can I mark emails forwarded to me by addy.io as spam?
No, you must not mark messages forwarded to you by addy.io as spam as this can damage the reputation of the mail servers and is against the [terms and conditions](https://addy.io/terms/).
If an alias is receiving spam messages then please deactivate it or delete it.
addy.io is signed up to multiple feedback loops (FBLs) that trigger a notification when any messages are marked as spam. Repeatedly marking messages as spam will result in your account being disabled.
## Can I use aliases to create multiple accounts on other websites and services?
No, you must not use addy.io to create large numbers of accounts on other websites/services as this is against the [terms and conditions](https://addy.io/terms/).
## Can I have multiple Free accounts?
Having multiple Free accounts is not considered an acceptable use of our service. Any users found to be abusing this rule may have their accounts disabled. This does not apply to those with a paid subscription.
## What if I don't want anyone to link ownership of my aliases together?
If you're concerned that your aliases are all linked by your username e.g. @johndoe.anonaddy.com, then you have a couple of options:
@ -288,20 +255,10 @@ To learn more about DMARC please see this site - [https://dmarc.org/](https://dm
If your addy.io recipient is with a popular mail service provider for example: Gmail, Outlook, Tutanota, Mailbox.org, Protonmail etc. then they will already have a DMARC policy in place so you do not need to take any action.
## I've been forwarded an email with a red warning banner saying it may have been spoofed, what does it mean?
If an incoming email looks like spam (for example, because it has failed its [DMARC](https://dmarc.org/overview/) check) then a red warning banner is added by addy.io before forwarding the message on to you. This warning banner is added in order to help protect you from any potential phishing attempts, for example someone pretending to be your bank.
Most of the time this is nothing to worry about and is just because the sender has not correctly configured their DNS records.
To see why this banner was added you can view the headers of the received email and look for the header called 'X-AnonAddy-Authentication-Results'. This header shows the original email's authentication results and will show you why the email failed its DMARC checks.
## Does addy.io strip out the banner information when I reply to an email?
Yes, the email banner "This email was sent to..." will be automatically removed when you reply to any messages. You can test this by replying to yourself from one of your aliases.
Make sure not to alter or edit the email banner as this may cause issues when trying to match and remove it. You can still remove it manually from the quoted message of your reply if you wish.
## How do I send email from an alias?
This works in the same way as replying to an email.
@ -364,10 +321,6 @@ You will not be able to activate any of the above again until you resubscribe.
When you subscribe you can choose which email to provide to Stripe, feel free to use an alias. This email will be used for notifications from Stripe such as; if your card payment fails or if your card has expired.
## Do you offer student discount?
Currently, addy.io does not offer any student discounts.
## How do you prevent spammers?
The following is in place to help prevent spam:
@ -409,7 +362,7 @@ If you get close to your limit (over 80%) you'll be sent an email letting you kn
## Can I login using an additional username?
Yes, you can login with any of your usernames. You can add 5 additional username as a Lite user and up to 20 additional usernames as a Pro user for totals of 6 and 21 respectively (including the one you signed up with).
Yes, you can login with any of your usernames. You can add 1 additional username as a Lite user and up to 10 additional usernames as a Pro user for totals of 2 and 11 respectively (including the one you signed up with).
## I'm not receiving any emails, what's wrong?
@ -502,7 +455,7 @@ For any other questions just send an email to - contact (at) help.addy.io ([GPG
* Postfix (3.0.0+) (plus postfix-mysql for database queries and postfix-pcre)
* PHP (8.2+) and the [php-mailparse](https://pecl.php.net/package/mailparse) extension, the [php-gnupg](https://pecl.php.net/package/gnupg) extension if you plan to encrypt forwarded emails, the [php-imagick](https://pecl.php.net/package/imagick) extension for generating 2FA QR codes
* Port 25 unblocked and open
* Redis (7.x+) for throttling and queues
* Redis (6.x+) for throttling and queues
* FQDN as hostname e.g. mail.anonaddy.me
* MariaDB / MySQL
* Nginx
@ -515,7 +468,7 @@ For full details please see the [self-hosting instructions file](SELF-HOSTING.md
## My sponsors
Thanks to [Vlad Timofeev](https://github.com/vlad-timofeev), [Patrick Dobler](https://github.com/patrickdobler), [Luca Steeb](https://github.com/steebchen), [narolinus](https://github.com/narolinus) and [Lukas](https://github.com/lunibo) for supporting me by sponsoring the project on GitHub!
Thanks to [Vlad Timofeev](https://github.com/vlad-timofeev), [Patrick Dobler](https://github.com/patrickdobler), [Luca Steeb](https://github.com/steebchen), [Laiteux](https://github.com/Laiteux), [narolinus](https://github.com/narolinus),[Limon Monte](https://github.com/limonte) and [Lukas](https://github.com/lunibo) for supporting me by sponsoring the project on GitHub!
Also an extra special thanks to [CrazyMax](https://github.com/crazy-max) for sponsoring me and also creating and maintaining the awesome [addy.io Docker image](https://github.com/anonaddy/docker)!

View file

@ -19,7 +19,6 @@
- [Enabling DANE by implementing DNSSEC and adding a TLSA record](#enabling-dane-by-implementing-dnssec-and-adding-a-tlsa-record)
- [Adding Certification Authority Authorization](#adding-certification-authority-authorization)
- [Updating](#updating)
- [Troubleshooting](#troubleshooting)
## Assumptions
@ -260,6 +259,7 @@ smtpd_recipient_restrictions =
reject_rhsbl_reverse_client dbl.spamhaus.org,
reject_rhsbl_sender dbl.spamhaus.org,
reject_rbl_client zen.spamhaus.org
reject_rbl_client dul.dnsbl.sorbs.net
# Block clients that speak too early.
smtpd_data_restrictions = reject_unauth_pipelining
@ -1356,29 +1356,18 @@ npm install
npm run production
# Run any database migrations
php artisan migrate --force
php artisan migrate
# Cache config, events, routes and views
php artisan optimize
# Clear cache
php artisan config:cache
php artisan view:cache
php artisan route:cache
php artisan event:cache
# Restart queue workers to reflect changes
php artisan queue:restart
```
## Troubleshooting
If you run into any problems then please check the following logs which should provide more information:
- `/var/www/anonaddy/storage/logs/laravel*.log` - Web application error logs (any errors relating to issues with the web application)
- `/var/log/mail.log` - Postfix mail logs (details of received and sent emails)
- `/var/log/mail.err` - Postfix errors (errors relating to Postfix configuration)
- `/var/log/php8.2-fpm.log` - PHP logs (logs relating to PHP FastCGI Process Manager)
- `/var/log/nginx/access.log` - Nginx access logs (log of client requests)
- `/var/log/nginx/error.log` - Nginx error logs (log of any server or request errors)
- `/var/log/supervisor/*.log` - Supervisor logs (log of any web application queue issues)
If a queued job (e.g. forwarding an email) fails, it is stored in the `failed_jobs` table in the database and can be [retried](https://laravel.com/docs/11.x/queues#retrying-failed-jobs).
## Credits
A big thank you to Xiao Guoan over at [linuxbabe.com](https://www.linuxbabe.com/) for all of his amazing articles. I highly recommend you subscribe to his newsletter.

View file

@ -44,7 +44,7 @@ class ClearFailedDeliveries extends Command
// Delete any stored failed deliveries older than 7 days
collect(Storage::disk('local')->listContents(''))
->each(function ($file) {
if ($file['type'] === 'file' && $file['lastModified'] < now()->subDays(7)->getTimestamp()) {
if ($file['type'] == 'file' && $file['lastModified'] < now()->subDays(7)->getTimestamp()) {
Storage::disk('local')->delete($file['path']);
}
});

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

@ -41,7 +41,7 @@ class EmailUsersWithTokenExpiringSoon extends Command
*/
public function handle()
{
User::with(['defaultUsername', 'defaultRecipient', 'tokens'])
User::with(['defaultUsername', 'defaultRecipient'])
->whereHas('tokens', function ($query) {
$query->whereDate('expires_at', now()->addWeek());
})

View file

@ -75,13 +75,13 @@ class ReceiveEmail extends Command
try {
$this->exitIfFromSelf();
$recipients = $this->getRecipients();
$file = $this->argument('file');
$this->parser = $this->getParser($file);
$this->senderFrom = $this->getSenderFrom();
$recipients = $this->getRecipients();
// Divide the size of the email by the number of recipients (excluding any unsubscribe recipients) to prevent it being added multiple times.
$recipientCount = $recipients->where('domain', '!=', 'unsubscribe.'.config('anonaddy.domain'))->count();
@ -200,9 +200,9 @@ class ReceiveEmail extends Command
}
if ($this->parser->getHeader('In-Reply-To') && $alias) {
$this->handleReply($user, $alias, $validEmailDestination);
$this->handleReply($user, $recipient, $alias);
} else {
$this->handleSendFrom($user, $recipient, $alias ?? null, $aliasable ?? null, $validEmailDestination);
$this->handleSendFrom($user, $recipient, $alias ?? null, $aliasable ?? null);
}
} elseif ($verifiedRecipient?->can_reply_send === false) {
// Notify user that they have not allowed this recipient to reply and send from aliases
@ -214,11 +214,11 @@ class ReceiveEmail extends Command
$this->handleForward($user, $recipient, $alias ?? null, $aliasable ?? null, $this->parser->getHeader('X-AnonAddy-Spam') === 'Yes');
}
}
} catch (\Throwable $e) {
$this->error('4.3.0 An error has occurred, please try again later.');
} catch (\Exception $e) {
report($e);
$this->error('4.3.0 An error has occurred, please try again later.');
exit(1);
}
}
@ -232,16 +232,18 @@ class ReceiveEmail extends Command
}
}
protected function handleReply($user, $alias, $destination)
protected function handleReply($user, $recipient, $alias)
{
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'R');
$sendTo = Str::replaceLast('=', '@', $recipient['extension']);
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size);
$message = new ReplyToEmail($user, $alias, $emailData);
Mail::to($destination)->queue($message);
Mail::to($sendTo)->queue($message);
}
protected function handleSendFrom($user, $recipient, $alias, $aliasable, $destination)
protected function handleSendFrom($user, $recipient, $alias, $aliasable)
{
if (is_null($alias)) {
$alias = $user->aliases()->create([
@ -256,11 +258,13 @@ class ReceiveEmail extends Command
$alias->refresh();
}
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'S');
$sendTo = Str::replaceLast('=', '@', $recipient['extension']);
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size);
$message = new SendFromEmail($user, $alias, $emailData);
Mail::to($destination)->queue($message);
Mail::to($sendTo)->queue($message);
}
protected function handleForward($user, $recipient, $alias, $aliasable, $isSpam)
@ -347,24 +351,12 @@ class ReceiveEmail extends Command
// Try to determine the bounce type, HARD, SPAM, SOFT
$bounceType = $this->getBounceType($dsn['Diagnostic-code'], $dsn['Status']);
$diagnosticCode = trim(Str::limit($dsn['Diagnostic-code'], 497));
$diagnosticCode = Str::limit($dsn['Diagnostic-code'], 497);
} else {
$bounceType = null;
$diagnosticCode = null;
}
// To sort '5.7.1 (delivery not authorized, message refused)' as status
if ($status = $dsn['Status'] ?? null) {
if (Str::length($status) > 5) {
if (is_null($diagnosticCode)) {
$diagnosticCode = trim(Str::substr($status, 5, 497));
}
$status = trim(Str::substr($status, 0, 5));
}
}
// Get the undelivered message
$undeliveredMessage = $attachments->filter(function ($attachment) {
return in_array($attachment->getContentType(), ['text/rfc822-headers', 'message/rfc822']);
@ -402,7 +394,7 @@ class ReceiveEmail extends Command
'sender' => $undeliveredMessageHeaders['X-anonaddy-original-sender'] ?? null,
'destination' => $bouncedEmailAddress,
'email_type' => $emailType,
'status' => $status ?? null,
'status' => $dsn['Status'] ?? null,
'code' => $diagnosticCode,
'attempted_at' => $outboundMessage->created_at,
]);
@ -454,7 +446,7 @@ class ReceiveEmail extends Command
}
if ($user->nearBandwidthLimit() && ! Cache::has("user:{$user->id}:near-bandwidth")) {
$user->notify(new NearBandwidthLimit);
$user->notify(new NearBandwidthLimit());
Cache::put("user:{$user->id}:near-bandwidth", now()->toDateTimeString(), now()->addDay());
}
@ -466,7 +458,8 @@ class ReceiveEmail extends Command
->allow(config('anonaddy.limit'))
->every(3600)
->then(
function () {},
function () {
},
function () use ($user) {
$user->update(['defer_until' => now()->addHour()]);
@ -491,7 +484,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) {
@ -512,7 +505,7 @@ class ReceiveEmail extends Command
return $next($mimePart);
});
if ($file === 'stream') {
if ($file == 'stream') {
$fd = fopen('php://stdin', 'r');
$this->rawEmail = '';
while (! feof($fd)) {
@ -556,12 +549,6 @@ class ReceiveEmail extends Command
protected function getBounceType($code, $status)
{
if (preg_match("/(:?mailbox|address|user|account|recipient|@).*(:?rejected|unknown|disabled|unavailable|invalid|inactive|not exist|does(n't| not) exist)|(:?rejected|unknown|unavailable|no|illegal|invalid|no such).*(:?mailbox|address|user|account|recipient|alias)|(:?address|user|recipient) does(n't| not) have .*(:?mailbox|account)|returned to sender|(:?auth).*(:?required)/i", $code)) {
// If the status starts with 4 then return soft instead of hard
if (Str::startsWith($status, '4')) {
return 'soft';
}
return 'hard';
}
@ -583,7 +570,7 @@ class ReceiveEmail extends Command
// Ensure contains '@', may be malformed header which causes sends/replies to fail
$address = $this->parser->getAddresses('from')[0]['address'];
return Str::contains($address, '@') && filter_var($address, FILTER_VALIDATE_EMAIL) ? $address : $this->option('sender');
return Str::contains($address, '@') ? $address : $this->option('sender');
} catch (\Exception $e) {
return $this->option('sender');
}

48
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,48 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
'App\Console\Commands\ResetBandwidth',
];
/**
* Define the application's command schedule.
*
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('anonaddy:reset-bandwidth')->monthlyOn(1, '00:00');
$schedule->command('anonaddy:check-domains-sending-verification')->daily();
$schedule->command('anonaddy:check-domains-mx-validation')->daily();
$schedule->command('anonaddy:clear-failed-deliveries')->daily();
$schedule->command('anonaddy:clear-outbound-messages')->everySixHours();
$schedule->command('anonaddy:email-users-with-token-expiring-soon')->daily();
$schedule->command('auth:clear-resets')->daily();
$schedule->command('sanctum:prune-expired --hours=168')->daily();
$schedule->command('cache:prune-stale-tags')->hourly();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View file

@ -4,29 +4,22 @@ namespace App\CustomMailDriver;
use App\CustomMailDriver\Mime\Crypto\AlreadyEncrypted;
use App\CustomMailDriver\Mime\Crypto\OpenPGPEncrypter;
use App\Models\Alias;
use App\Models\OutboundMessage;
use App\Models\Recipient;
use App\Models\User;
use App\Notifications\FailedDeliveryNotification;
use App\Notifications\GpgKeyExpired;
use Exception;
use Illuminate\Contracts\Mail\Mailable as MailableContract;
use Illuminate\Mail\Mailer;
use Illuminate\Mail\SentMessage;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use ParagonIE\ConstantTime\Base32;
use Ramsey\Uuid\Uuid;
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
{
private $data;
/**
* Send a new message using a view.
*
@ -36,8 +29,6 @@ class CustomMailer extends Mailer
*/
public function send($view, array $data = [], $callback = null)
{
$this->data = $data;
if ($view instanceof MailableContract) {
return $this->sendMailable($view);
}
@ -76,19 +67,17 @@ class CustomMailer extends Mailer
try {
$encrypter = new OpenPGPEncrypter(config('anonaddy.signing_key_fingerprint'), $data['fingerprint'], '~/.gnupg', $recipient->protected_headers);
$encryptedSymfonyMessage = $recipient->inline_encryption ? $encrypter->encryptInline($symfonyMessage) : $encrypter->encrypt($symfonyMessage);
} catch (Exception $e) {
} catch (RuntimeException $e) {
info($e->getMessage());
$encryptedSymfonyMessage = null;
$encrypter = null;
$recipient->update(['should_encrypt' => false]);
$recipient->notify(new GpgKeyExpired);
$recipient->notify(new GpgKeyExpired());
}
if ($encryptedSymfonyMessage) {
$symfonyMessage = $encryptedSymfonyMessage;
if ($encrypter) {
$symfonyMessage = $recipient->inline_encryption ? $encrypter->encryptInline($symfonyMessage) : $encrypter->encrypt($symfonyMessage);
}
}
@ -98,12 +87,11 @@ class CustomMailer extends Mailer
}
// DkimSigner only for forwards, replies and sends...
if (isset($data['needsDkimSignature']) && $data['needsDkimSignature'] && ! is_null(config('anonaddy.dkim_signing_key'))) {
if (isset($data['needsDkimSignature']) && $data['needsDkimSignature']) {
$dkimSigner = new DkimSigner(config('anonaddy.dkim_signing_key'), $data['aliasDomain'], config('anonaddy.dkim_selector'));
$options = (new DkimOptions)->headersToIgnore([
$options = (new DkimOptions())->headersToIgnore([
'List-Unsubscribe',
'List-Unsubscribe-Post',
'Return-Path',
'Feedback-ID',
'Content-Type',
@ -139,82 +127,12 @@ class CustomMailer extends Mailer
// If the message is a forward, reply or send then use the verp domain
if (isset($data['emailType']) && in_array($data['emailType'], ['F', 'R', 'S'])) {
$symfonyMessage->returnPath($verpLocalPart.'@'.$data['verpDomain']);
$message->returnPath($verpLocalPart.'@'.$data['verpDomain']);
} else {
$symfonyMessage->returnPath($verpLocalPart.'@'.config('anonaddy.domain'));
$message->returnPath($verpLocalPart.'@'.config('anonaddy.domain'));
}
try {
$symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage);
} catch (Exception $e) {
$symfonySentMessage = false;
$userId = $data['userId'] ?? '';
// Store the undelivered message if enabled by user. Do not store email verification notifications.
if ($user = User::find($userId)) {
$failedDeliveryId = Uuid::uuid4();
// Example $e->getMessage();
// Expected response code "250/251/252" but got code "554", with message "554 5.7.1 Spam message rejected".
// Expected response code "250" but got empty code.
// Connection could not be established with host "mail.example:25": stream_socket_client(): Unable to connect to mail.example.com:25 (Connection refused)
$matches = Str::of($e->getMessage())->matchAll('/"([^"]*)"/');
$status = $matches[1] ?? '4.3.2';
$code = $matches[2] ?? '453 4.3.2 A temporary error has occurred.';
if ($code && $status) {
// If the error is temporary e.g. connection lost then rethrow the error to allow retry or send to failed_jobs table
if (Str::startsWith($status, '4')) {
throw $e;
}
// Try to determine the bounce type, HARD, SPAM, SOFT
$bounceType = $this->getBounceType($code, $status);
$diagnosticCode = Str::limit($code, 497);
} else {
$bounceType = null;
$diagnosticCode = null;
}
$emailType = $data['emailType'] ?? null;
if ($user->store_failed_deliveries && ! in_array($emailType, ['VR', 'VU'])) {
$isStored = Storage::disk('local')->put("{$failedDeliveryId}.eml", $symfonyMessage->toString());
}
$failedDelivery = $user->failedDeliveries()->create([
'id' => $failedDeliveryId,
'recipient_id' => $data['recipientId'] ?? null,
'alias_id' => $data['aliasId'] ?? null,
'is_stored' => $isStored ?? false,
'bounce_type' => $bounceType,
'remote_mta' => config('mail.mailers.smtp.host'),
'sender' => $symfonyMessage->getHeaders()->get('X-AnonAddy-Original-Sender')?->getValue(),
'destination' => $symfonyMessage->getTo()[0]?->getAddress(),
'email_type' => $emailType,
'status' => $status,
'code' => $diagnosticCode,
'attempted_at' => now(),
]);
// Calling $failedDelivery->email_type will return 'Failed Delivery' and not 'FDN'
// Check if the bounce is a Failed delivery notification or Alias deactivated notification and if so do not notify the user again
if (! in_array($emailType, ['FDN', 'ADN']) && ! is_null($emailType)) {
$recipient = Recipient::find($failedDelivery->recipient_id);
$alias = Alias::find($failedDelivery->alias_id);
$notifiable = $recipient?->email_verified_at ? $recipient : $user?->defaultRecipient;
// Notify user of failed delivery
if ($notifiable?->email_verified_at) {
$notifiable->notify(new FailedDeliveryNotification($alias->email ?? null, $failedDelivery->sender, $symfonyMessage->getSubject(), $failedDelivery?->is_stored, $user?->store_failed_deliveries, $recipient?->email));
}
}
}
}
$symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage);
if ($symfonySentMessage) {
$sentMessage = new SentMessage($symfonySentMessage);
@ -251,24 +169,10 @@ class CustomMailer extends Mailer
{
try {
$envelopeMessage = clone $message;
// Add in original Tos that have been updated
if ($tos = $this->data['tos'] ?? null) {
foreach ($tos as $key => $to) {
if ($key === 0) {
// This allows us to have the To: header set as the alias whilst still delivering to the correct RCPT TO for forwards.
$message->to($to); // In order to override recipient email for forwards
} else {
$message->addTo($to);
}
}
}
// Add in original CCs that have been updated
if ($ccs = $this->data['ccs'] ?? null) {
foreach ($ccs as $cc) {
$message->addCc($cc);
}
// 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');
}
// Add the original sender header here to prevent it altering the envelope from address
@ -292,28 +196,4 @@ class CustomMailer extends Mailer
return "b_{$encodedPayload}_{$encodedSignature}";
}
protected function getBounceType($code, $status)
{
if (preg_match("/(:?mailbox|address|user|account|recipient|@).*(:?rejected|unknown|disabled|unavailable|invalid|inactive|not exist|does(n't| not) exist)|(:?rejected|unknown|unavailable|no|illegal|invalid|no such).*(:?mailbox|address|user|account|recipient|alias)|(:?address|user|recipient) does(n't| not) have .*(:?mailbox|account)|returned to sender|(:?auth).*(:?required)/i", $code)) {
// If the status starts with 4 then return soft instead of hard
if (Str::startsWith($status, '4')) {
return 'soft';
}
return 'hard';
}
if (preg_match('/(:?spam|unsolicited|blacklisting|blacklisted|blacklist|554|mail content denied|reject for policy reason|mail rejected by destination domain|security issue)/i', $code)) {
return 'spam';
}
// No match for code but status starts with 5 e.g. 5.2.2
if (Str::startsWith($status, '5')) {
return 'hard';
}
return 'soft';
}
}

View file

@ -101,13 +101,11 @@ class OpenPGPEncrypter
* @param Email $email
* @return $this
*
* @throws Exception
* @throws RuntimeException
*/
public function encrypt(Email $symfonyMessage): Email
public function encrypt(Email $message): Email
{
$originalMessage = clone $symfonyMessage;
// Clone to ensure headers are not altered if encryption fails
$message = clone $symfonyMessage;
$originalMessage = clone $message;
$headers = $message->getPreparedHeaders();
@ -172,12 +170,12 @@ class OpenPGPEncrypter
* @param Email $email
* @return $this
*
* @throws Exception
* @throws RuntimeException
*/
public function encryptInline(Email $symfonyMessage): Email
public function encryptInline(Email $message): Email
{
if (! $this->signingKey) {
foreach ($symfonyMessage->getFrom() as $key => $value) {
foreach ($message->getFrom() as $key => $value) {
$this->addSignature($this->getKey($key, 'sign'));
}
}
@ -190,16 +188,16 @@ class OpenPGPEncrypter
throw new RuntimeException('Encryption has been enabled, but no recipients have been added. Use autoAddRecipients() or addRecipient()');
}
$body = $symfonyMessage->getTextBody() ?? '';
$body = $message->getTextBody() ?? '';
$text = $this->pgpEncryptAndSignString($body, $this->recipientKey, $this->signingKey);
$headers = $symfonyMessage->getPreparedHeaders();
$headers = $message->getPreparedHeaders();
$headers->setHeaderBody('Parameterized', 'Content-Type', 'text/plain');
$headers->setHeaderParameter('Content-Type', 'charset', 'utf-8');
$symfonyMessage->setHeaders($headers);
$message->setHeaders($headers);
return $symfonyMessage->setBody(new EncryptedPart($text));
return $message->setBody(new EncryptedPart($text));
}
/**
@ -220,15 +218,15 @@ class OpenPGPEncrypter
}
if (! $this->gnupg) {
$this->gnupg = new \gnupg;
$this->gnupg = new \gnupg();
}
$this->gnupg->seterrormode(\gnupg::ERROR_EXCEPTION);
}
/**
* @param $plaintext
* @param $keyFingerprints
* @param $plaintext
* @param $keyFingerprints
* @return string
*
* @throws RuntimeException
@ -290,6 +288,6 @@ class OpenPGPEncrypter
protected function isValidKey($key, $purpose)
{
return ! ($key['disabled'] || $key['expired'] || $key['revoked'] || ($purpose === 'sign' && ! $key['can_sign']) || ($purpose === 'encrypt' && ! $key['can_encrypt']));
return ! ($key['disabled'] || $key['expired'] || $key['revoked'] || ($purpose == 'sign' && ! $key['can_sign']) || ($purpose == 'encrypt' && ! $key['can_encrypt']));
}
}

View file

@ -86,7 +86,7 @@ class EncryptedPart extends AbstractPart
public function getPreparedHeaders(): Headers
{
return clone new Headers;
return clone new Headers();
}
public function asDebugString(): string
@ -104,7 +104,7 @@ class EncryptedPart extends AbstractPart
private function getEncoder(): ContentEncoderInterface
{
return new RawContentEncoder;
return new RawContentEncoder();
}
public function __sleep(): array

View file

@ -11,5 +11,4 @@ enum DisplayFromFormat: int
case ADDRESS = 4;
case NONE = 5;
case DOMAINONLY = 6;
case LEGACY = 7;
}

View file

@ -1,12 +0,0 @@
<?php
namespace App\Enums;
enum LoginRedirect: int
{
case DEFAULT = 0;
case ALIASES = 1;
case RECIPIENTS = 2;
case USERNAMES = 3;
case DOMAINS = 4;
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
//
}
}

View file

@ -3,7 +3,6 @@
namespace App\Helpers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;
@ -19,20 +18,6 @@ class GitVersionHelper
return self::cacheFreshVersion();
}
public static function updateAvailable()
{
$currentVersion = self::version()->value();
// Cache latestVersion for 1 day
$latestVersion = Cache::remember('app-latest-version', now()->addDay(), function () {
$response = Http::get('https://api.github.com/repos/anonaddy/anonaddy/releases/latest');
return Str::of($response->json('tag_name', 'v0.0.0'))->after('v')->trim();
});
return version_compare($latestVersion, $currentVersion, '>');
}
public static function cacheFreshVersion()
{
$version = self::freshVersion();

View file

@ -1,7 +1,6 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
function user()
{
@ -26,17 +25,3 @@ function randomString(int $length): string
return $str;
}
function stripEmailExtension(string $email): string
{
if (! Str::contains($email, '@')) {
return $email;
}
// Strip the email of extensions
[$localPart, $domain] = explode('@', strtolower($email));
// Remove plus extension from local part if present
$localPart = Str::contains($localPart, '+') ? Str::before($localPart, '+') : $localPart;
return $localPart.'@'.$domain;
}

View file

@ -13,6 +13,6 @@ class AliasExportController extends Controller
return back()->withErrors(['aliases_export' => 'You don\'t have any aliases to export.']);
}
return Excel::download(new AliasesExport, 'aliases-'.now()->toDateString().'.csv');
return Excel::download(new AliasesExport(), 'aliases-'.now()->toDateString().'.csv');
}
}

View file

@ -16,7 +16,7 @@ class ActiveDomainController extends Controller
$domain->activate();
return new DomainResource($domain->load('defaultRecipient')->loadCount('aliases'));
return new DomainResource($domain->load(['aliases', 'defaultRecipient']));
}
public function destroy($id)

View file

@ -16,7 +16,7 @@ class ActiveUsernameController extends Controller
$username->activate();
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
}
public function destroy($id)

View file

@ -3,10 +3,9 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\GeneralAliasBulkRequest;
use App\Http\Requests\RecipientsAliasBulkRequest;
use App\Http\Resources\AliasResource;
use App\Rules\VerifiedRecipientId;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Ramsey\Uuid\Uuid;
@ -17,8 +16,13 @@ class AliasBulkController extends Controller
$this->middleware('throttle:12,1');
}
public function get(GeneralAliasBulkRequest $request)
public function get(Request $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliases = user()->aliases()->withTrashed()
->whereIn('id', $request->ids)
->get();
@ -31,8 +35,13 @@ class AliasBulkController extends Controller
return AliasResource::collection($aliases);
}
public function activate(GeneralAliasBulkRequest $request)
public function activate(Request $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasesWithTrashed = user()->aliases()->withTrashed()
->select(['id', 'user_id', 'active', 'deleted_at'])
->where('active', false)
@ -66,8 +75,13 @@ class AliasBulkController extends Controller
], 200);
}
public function deactivate(GeneralAliasBulkRequest $request)
public function deactivate(Request $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasIds = user()->aliases()
->where('active', true)
->whereIn('id', $request->ids)
@ -86,8 +100,13 @@ class AliasBulkController extends Controller
], 200);
}
public function delete(GeneralAliasBulkRequest $request)
public function delete(Request $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasIds = user()->aliases()
->whereIn('id', $request->ids)
->pluck('id');
@ -110,8 +129,13 @@ class AliasBulkController extends Controller
], 200);
}
public function forget(GeneralAliasBulkRequest $request)
public function forget(Request $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasIds = user()->aliases()->withTrashed()
->whereIn('id', $request->ids)
->pluck('id');
@ -136,10 +160,6 @@ class AliasBulkController extends Controller
'emails_blocked' => 0,
'emails_replied' => 0,
'emails_sent' => 0,
'last_forwarded' => null,
'last_blocked' => null,
'last_replied' => null,
'last_sent' => null,
'active' => false,
'deleted_at' => now(),
]);
@ -159,8 +179,13 @@ class AliasBulkController extends Controller
], 200);
}
public function restore(GeneralAliasBulkRequest $request)
public function restore(Request $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasIds = user()->aliases()->onlyTrashed()
->whereIn('id', $request->ids)
->pluck('id');
@ -180,7 +205,7 @@ class AliasBulkController extends Controller
], 200);
}
public function recipients(RecipientsAliasBulkRequest $request)
public function recipients(Request $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
@ -188,7 +213,7 @@ class AliasBulkController extends Controller
'recipient_ids' => [
'array',
'max:10',
new VerifiedRecipientId,
new VerifiedRecipientId(),
],
'recipient_ids.*' => 'required|uuid|distinct',
]);

View file

@ -17,42 +17,10 @@ class AliasController extends Controller
public function index(IndexAliasRequest $request)
{
$aliases = user()->aliases()->with('recipients')
->when($request->input('recipient'), function ($query, $id) {
return $query->usesRecipientWithId($id, $id === user()->default_recipient_id);
})
->when($request->input('domain'), function ($query, $id) {
return $query->belongsToAliasable('App\Models\Domain', $id);
})
->when($request->input('username'), function ($query, $id) {
return $query->belongsToAliasable('App\Models\Username', $id);
})
->when($request->input('sort'), function ($query, $sort) {
$direction = strpos($sort, '-') === 0 ? 'desc' : 'asc';
$sort = ltrim($sort, '-');
$compareOperator = $direction === 'desc' ? '>' : '<';
// If sort is last_used then order by all and return
if ($sort === 'last_used') {
return $query
->orderByRaw(
"CASE
WHEN (last_forwarded {$compareOperator} last_replied
OR (last_forwarded IS NOT NULL
AND last_replied IS NULL))
AND (last_forwarded {$compareOperator} last_sent
OR (last_forwarded IS NOT NULL
AND last_sent IS NULL))
THEN last_forwarded
WHEN last_replied {$compareOperator} last_sent
OR (last_replied IS NOT NULL
AND last_sent IS NULL)
THEN last_replied
ELSE last_sent
END {$direction}"
)->orderBy('created_at', 'desc');
}
// If sort is created at then simply return as no need for secondary sorting below
if ($sort === 'created_at') {
return $query->orderBy($sort, $direction);
}
@ -234,13 +202,10 @@ class AliasController extends Controller
'emails_blocked' => 0,
'emails_replied' => 0,
'emails_sent' => 0,
'last_forwarded' => null,
'last_blocked' => null,
'last_replied' => null,
'last_sent' => null,
'active' => false,
'deleted_at' => now(), // Soft delete to prevent from being regenerated
]);
// Soft delete to prevent from being regenerated
$alias->delete();
} else {
$alias->forceDelete();
}

View file

@ -16,7 +16,7 @@ class AllowedRecipientController extends Controller
$recipient->update(['can_reply_send' => true]);
return new RecipientResource($recipient->loadCount('aliases'));
return new RecipientResource($recipient->load('aliases'));
}
public function destroy($id)

View file

@ -16,7 +16,7 @@ class CatchAllDomainController extends Controller
$domain->enableCatchAll();
return new DomainResource($domain->load('defaultRecipient')->loadCount('aliases'));
return new DomainResource($domain->load(['aliases', 'defaultRecipient']));
}
public function destroy($id)

View file

@ -16,7 +16,7 @@ class CatchAllUsernameController extends Controller
$username->enableCatchAll();
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
}
public function destroy($id)

View file

@ -17,19 +17,19 @@ class DomainController extends Controller
public function index()
{
return DomainResource::collection(user()->domains()->with('defaultRecipient')->withCount('aliases')->latest()->get());
return DomainResource::collection(user()->domains()->with(['aliases', 'defaultRecipient'])->latest()->get());
}
public function show($id)
{
$domain = user()->domains()->findOrFail($id);
return new DomainResource($domain->load('defaultRecipient')->loadCount('aliases'));
return new DomainResource($domain->load(['aliases', 'defaultRecipient']));
}
public function store(StoreDomainRequest $request)
{
$domain = new Domain;
$domain = new Domain();
$domain->domain = $request->domain;
if (! $domain->checkVerification()) {
@ -40,7 +40,7 @@ class DomainController extends Controller
$domain->markDomainAsVerified();
return new DomainResource($domain->refresh()->load('defaultRecipient')->loadCount('aliases'));
return new DomainResource($domain->refresh()->load(['aliases', 'defaultRecipient']));
}
public function update(UpdateDomainRequest $request, $id)
@ -55,13 +55,9 @@ class DomainController extends Controller
$domain->from_name = $request->from_name;
}
if ($request->has('auto_create_regex')) {
$domain->auto_create_regex = $request->auto_create_regex;
}
$domain->save();
return new DomainResource($domain->refresh()->load('defaultRecipient')->loadCount('aliases'));
return new DomainResource($domain->refresh()->load(['aliases', 'defaultRecipient']));
}
public function destroy($id)

View file

@ -20,6 +20,6 @@ class DomainDefaultRecipientController extends Controller
$domain->save();
return new DomainResource($domain->load('defaultRecipient')->loadCount('aliases'));
return new DomainResource($domain->load(['aliases', 'defaultRecipient']));
}
}

View file

@ -10,7 +10,6 @@ class DomainOptionController extends Controller
{
return response()->json([
'data' => user()->domainOptions(),
'sharedDomains' => user()->sharedDomainOptions(),
'defaultAliasDomain' => user()->default_alias_domain,
'defaultAliasFormat' => user()->default_alias_format,
]);

View file

@ -20,7 +20,7 @@ class EncryptedRecipientController extends Controller
$recipient->update(['should_encrypt' => true]);
return new RecipientResource($recipient->loadCount('aliases'));
return new RecipientResource($recipient->load('aliases'));
}
public function destroy($id)

View file

@ -24,7 +24,7 @@ class InlineEncryptedRecipientController extends Controller
$recipient->update(['inline_encryption' => true]);
return new RecipientResource($recipient->loadCount('aliases'));
return new RecipientResource($recipient->load('aliases'));
}
public function destroy($id)

View file

@ -16,7 +16,7 @@ class LoginableUsernameController extends Controller
$username->allowLogin();
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
}
public function destroy($id)

View file

@ -24,7 +24,7 @@ class ProtectedHeadersRecipientController extends Controller
$recipient->update(['protected_headers' => true]);
return new RecipientResource($recipient->loadCount('aliases'));
return new RecipientResource($recipient->load('aliases'));
}
public function destroy($id)

View file

@ -11,7 +11,7 @@ class RecipientController extends Controller
{
public function index(IndexRecipientRequest $request)
{
$recipients = user()->recipients()->withCount('aliases')->latest();
$recipients = user()->recipients()->with('aliases')->latest();
if ($request->input('filter.verified') === 'true') {
$recipients->verified();
@ -28,7 +28,7 @@ class RecipientController extends Controller
{
$recipient = user()->recipients()->findOrFail($id);
return new RecipientResource($recipient->loadCount('aliases'));
return new RecipientResource($recipient->load('aliases'));
}
public function store(StoreRecipientRequest $request)
@ -45,7 +45,7 @@ class RecipientController extends Controller
$recipient->sendEmailVerificationNotification();
}
return new RecipientResource($recipient->refresh()->loadCount('aliases'));
return new RecipientResource($recipient->refresh()->load('aliases'));
}
public function destroy($id)

View file

@ -12,7 +12,7 @@ class RecipientKeyController extends Controller
public function __construct()
{
$this->gnupg = new \gnupg;
$this->gnupg = new \gnupg();
}
public function update(UpdateRecipientKeyRequest $request, $id)
@ -30,7 +30,7 @@ class RecipientKeyController extends Controller
'fingerprint' => $info['fingerprint'],
]);
return new RecipientResource($recipient->fresh()->loadCount('aliases'));
return new RecipientResource($recipient->fresh()->load('aliases'));
}
public function destroy($id)

View file

@ -11,14 +11,14 @@ class UsernameController extends Controller
{
public function index()
{
return UsernameResource::collection(user()->usernames()->with('defaultRecipient')->withCount('aliases')->latest()->get());
return UsernameResource::collection(user()->usernames()->with(['aliases', 'defaultRecipient'])->latest()->get());
}
public function show($id)
{
$username = user()->usernames()->findOrFail($id);
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
}
public function store(StoreUsernameRequest $request)
@ -31,7 +31,7 @@ class UsernameController extends Controller
user()->increment('username_count');
return new UsernameResource($username->refresh()->load('defaultRecipient')->loadCount('aliases'));
return new UsernameResource($username->refresh()->load(['aliases', 'defaultRecipient']));
}
public function update(UpdateUsernameRequest $request, $id)
@ -46,13 +46,9 @@ class UsernameController extends Controller
$username->from_name = $request->from_name;
}
if ($request->has('auto_create_regex')) {
$username->auto_create_regex = $request->auto_create_regex;
}
$username->save();
return new UsernameResource($username->refresh()->load('defaultRecipient')->loadCount('aliases'));
return new UsernameResource($username->refresh()->load(['aliases', 'defaultRecipient']));
}
public function destroy($id)

View file

@ -20,6 +20,6 @@ class UsernameDefaultRecipientController extends Controller
$username->save();
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
}
}

View file

@ -6,12 +6,9 @@ use App\Facades\Webauthn;
use App\Http\Controllers\Controller;
use App\Http\Requests\ApiAuthenticationLoginRequest;
use App\Http\Requests\ApiAuthenticationMfaRequest;
use App\Http\Requests\DestroyAccountRequest;
use App\Jobs\DeleteAccount;
use App\Models\User;
use App\Models\Username;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
@ -23,58 +20,35 @@ class ApiAuthenticationController extends Controller
public function __construct()
{
$this->middleware('throttle:3,1');
$this->middleware(['auth:sanctum', 'verified'])->only(['logout', 'destroy']);
}
public function login(ApiAuthenticationLoginRequest $request)
{
$user = Username::select(['user_id', 'username'])->firstWhere('username', $request->username)?->user;
$user = Username::firstWhere('username', $request->username)?->user;
if (! $user || ! Hash::check($request->password, $user->password)) {
return response()->json([
'message' => 'The provided credentials are incorrect.',
], 401);
}
if (! $user->hasVerifiedDefaultRecipient()) {
return response()->json([
'message' => 'Your email address is not verified.',
'error' => 'The provided credentials are incorrect',
], 401);
}
// Check if user has 2FA enabled, if needs OTP then return mfa_key
if ($user->two_factor_enabled) {
return response()->json([
'message' => "OTP required, please make a request to /api/auth/mfa with the 'mfa_key', 'otp' and 'device_name' including a 'X-CSRF-TOKEN' header.",
'message' => "OTP required, please make a request to /api/auth/mfa with the 'mfa_key', 'otp' and 'device_name' including a 'X-CSRF-TOKEN' header",
'mfa_key' => Crypt::encryptString($user->id.'|'.config('anonaddy.secret').'|'.Carbon::now()->addMinutes(5)->getTimestamp()),
'csrf_token' => csrf_token(),
], 422);
} elseif (Webauthn::enabled($user)) {
// If WebAuthn is enabled then return currently unsupported message
return response()->json([
'message' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead.',
'error' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead',
], 403);
}
// day, week, month, year or null
if ($request->expiration) {
$method = 'add'.ucfirst($request->expiration);
$expiration = now()->{$method}();
} else {
$expiration = null;
}
// Token expires after 3 months, user must re-login
$newToken = $user->createToken($request->device_name, ['*'], $expiration);
$token = $newToken->accessToken;
// If the user doesn't use 2FA then return the new API key
return response()->json([
'api_key' => explode('|', $newToken->plainTextToken, 2)[1],
'name' => $token->name,
'created_at' => $token->created_at?->toDateTimeString(),
'expires_at' => $token->expires_at?->toDateTimeString(),
'api_key' => explode('|', $user->createToken($request->device_name)->plainTextToken, 2)[1],
]);
}
@ -84,7 +58,7 @@ class ApiAuthenticationController extends Controller
$mfaKey = Crypt::decryptString($request->mfa_key);
} catch (DecryptException $e) {
return response()->json([
'message' => 'Invalid mfa_key.',
'error' => 'Invalid mfa_key',
], 401);
}
$parts = explode('|', $mfaKey, 3);
@ -92,29 +66,26 @@ class ApiAuthenticationController extends Controller
$user = User::find($parts[0]);
if (! $user || $parts[1] !== config('anonaddy.secret')) {
return response()->json([
'message' => 'Invalid mfa_key.',
'error' => 'Invalid mfa_key',
], 401);
}
// Check if the mfa_key has expired
if (Carbon::now()->getTimestamp() > $parts[2]) {
return response()->json([
'message' => 'mfa_key expired, please request a new one at /api/auth/login.',
'error' => 'mfa_key expired, please request a new one at /api/auth/login',
], 401);
}
$google2fa = new Google2FA;
$lastTimeStamp = Cache::get('2fa_ts:'.$user->id, 0);
$google2fa = new Google2FA();
$lastTimeStamp = Cache::get('2fa_ts:'.$user->id);
$timestamp = $google2fa->verifyKeyNewer($user->two_factor_secret, $request->otp, $lastTimeStamp, config('google2fa.window'));
$timestamp = $google2fa->verifyKeyNewer($user->two_factor_secret, $request->otp, $lastTimeStamp);
if (! $timestamp) {
return response()->json([
'message' => 'The \'One Time Password\' typed was wrong.',
'error' => 'The \'One Time Password\' typed was wrong',
], 401);
}
@ -122,44 +93,8 @@ class ApiAuthenticationController extends Controller
Cache::put('2fa_ts:'.$user->id, $timestamp, now()->addMinutes(5));
}
// day, week, month, year or null
if ($request->expiration) {
$method = 'add'.ucfirst($request->expiration);
$expiration = now()->{$method}();
} else {
$expiration = null;
}
$newToken = $user->createToken($request->device_name, ['*'], $expiration);
$token = $newToken->accessToken;
return response()->json([
'api_key' => explode('|', $newToken->plainTextToken, 2)[1],
'name' => $token->name,
'created_at' => $token->created_at?->toDateTimeString(),
'expires_at' => $token->expires_at?->toDateTimeString(),
'api_key' => explode('|', $user->createToken($request->device_name)->plainTextToken, 2)[1],
]);
}
public function logout(Request $request)
{
$token = $request->user()?->currentAccessToken();
if (! $token) {
return response()->json([
'message', 'API key not found.',
], 404);
}
$token->delete();
return response()->json([], 204);
}
public function destroy(DestroyAccountRequest $request)
{
DeleteAccount::dispatch($request->user());
return response()->json([], 204);
}
}

View file

@ -14,7 +14,7 @@ class BackupCodeController extends Controller
public function __construct()
{
$this->middleware('auth');
$this->middleware('throttle:3,1')->only(['login', 'update']);
$this->middleware('throttle:3,1')->only('login');
}
public function index(Request $request)

View file

@ -2,11 +2,11 @@
namespace App\Http\Controllers\Auth;
use App\Enums\LoginRedirect;
use App\Http\Controllers\Controller;
use App\Models\Username;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
@ -43,18 +43,6 @@ class LoginController extends Controller
$this->middleware('guest')->except('logout');
}
public function redirectTo()
{
// Dynamic redirect setting to allow users to choose to go to /aliases page instead etc.
return match (user()->login_redirect) {
LoginRedirect::ALIASES => '/aliases',
LoginRedirect::RECIPIENTS => '/recipients',
LoginRedirect::USERNAMES => '/usernames',
LoginRedirect::DOMAINS => '/domains',
default => '/',
};
}
public function username()
{
return 'id';
@ -100,28 +88,6 @@ class LoginController extends Controller
return $request->only('id', 'password');
}
/**
* Send the response after the user was authenticated.
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();
$this->clearLoginAttempts($request);
if ($response = $this->authenticated($request, $this->guard()->user())) {
return $response;
}
// If the intended path is just the dashboard then ignore and use the user's login redirect instead
$redirectTo = $this->redirectTo();
$intended = session()->pull('url.intended');
return $intended === url('/') ? redirect()->to($redirectTo) : redirect()->intended($intended ?? $redirectTo);
}
/**
* Get the failed login response instance.
*
@ -136,6 +102,20 @@ class LoginController extends Controller
]);
}
/**
* The user has been authenticated.
*
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user)
{
// Check if the user's password needs rehashing
if (Hash::needsRehash($user->password)) {
$user->update(['password' => Hash::make($request->password)]);
}
}
/**
* The user has logged out of the application.
*

View file

@ -36,7 +36,7 @@ class PersonalAccessTokenController extends Controller
return [
'token' => new PersonalAccessTokenResource($token->accessToken),
'accessToken' => $accessToken,
'qrCode' => (new QRCode)->render(config('app.url').'|'.$accessToken),
'qrCode' => (new QRCode())->render(config('app.url').'|'.$accessToken),
];
}

View file

@ -74,22 +74,20 @@ class RegisterController extends Controller
return Validator::make($data, [
'username' => [
'bail',
'required',
'regex:/^[a-zA-Z0-9]*$/',
'max:20',
'unique:usernames,username',
new NotBlacklisted,
new NotDeletedUsername,
new NotBlacklisted(),
new NotDeletedUsername(),
],
'email' => [
'bail',
'required',
'email:rfc,dns',
'max:254',
'confirmed',
new RegisterUniqueRecipient,
new NotLocalRecipient,
new RegisterUniqueRecipient(),
new NotLocalRecipient(),
],
'password' => ['required', Password::defaults()],
], [

View file

@ -16,7 +16,6 @@ class TwoFactorAuthController extends Controller
public function __construct(Request $request)
{
$this->middleware('throttle:3,1')->only(['store', 'update', 'destroy']);
$this->twoFactor = app('pragmarx.google2fa');
$this->authenticator = app(Authenticator::class)->boot($request);
}
@ -28,7 +27,7 @@ class TwoFactorAuthController extends Controller
public function store(EnableTwoFactorAuthRequest $request)
{
if (! $this->twoFactor->verifyKey(user()->two_factor_secret, $request->two_factor_token, config('google2fa.window'))) {
if (! $this->twoFactor->verifyKey(user()->two_factor_secret, $request->two_factor_token)) {
return redirect(url()->previous().'#two-factor')->withErrors(['two_factor_token' => 'The token you entered was incorrect']);
}

View file

@ -12,7 +12,6 @@ use Illuminate\Foundation\Auth\VerifiesEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Inertia\Inertia;
class VerificationController extends Controller
@ -44,7 +43,7 @@ class VerificationController extends Controller
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('auth')->except('verify');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:1,1')->only('resend');
$this->middleware('throttle:6,1')->only('verify');
@ -107,8 +106,7 @@ class VerificationController extends Controller
$user = $verifiable->user;
$defaultRecipient = $user->defaultRecipient;
// Notify the current default recipient of the change
// Have to use sendNow method here to ensure this notification is sent before the current defaultRecipient's email is updated below
Notification::sendNow($defaultRecipient, new DefaultRecipientUpdated($verifiable->email));
$defaultRecipient->notify(new DefaultRecipientUpdated($verifiable->email));
// Set verifiable email as new default recipient
$defaultRecipient->update([

View file

@ -14,11 +14,6 @@ use LaravelWebauthn\Http\Requests\WebauthnRegisterRequest;
class WebauthnController extends ControllersWebauthnController
{
public function __construct()
{
$this->middleware('throttle:3,1')->only('destroy');
}
public function index()
{
return user()->webauthnKeys()->latest()->select(['id', 'name', 'enabled', 'created_at'])->get()->values();

View file

@ -7,11 +7,6 @@ use Illuminate\Http\Request;
class WebauthnEnabledKeyController extends Controller
{
public function __construct()
{
$this->middleware('throttle:3,1')->only('destroy');
}
public function store(Request $request)
{
$webauthnKey = user()->webauthnKeys()->findOrFail($request->id);

View file

@ -7,11 +7,6 @@ use Illuminate\Support\Facades\Auth;
class BrowserSessionController extends Controller
{
public function __construct()
{
$this->middleware('throttle:3,1')->only('destroy');
}
public function destroy(Request $request)
{
$request->validate([

View file

@ -1,17 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Enums\LoginRedirect;
use App\Http\Requests\UpdateLoginRedirectRequest;
class LoginRedirectController extends Controller
{
public function update(UpdateLoginRedirectRequest $request)
{
user()->login_redirect = LoginRedirect::from($request->redirect);
user()->save();
return back()->with(['flash' => 'Login Redirect Updated Successfully']);
}
}

View file

@ -8,11 +8,6 @@ use Illuminate\Support\Facades\Hash;
class PasswordController extends Controller
{
public function __construct()
{
$this->middleware('throttle:3,1')->only('update');
}
public function update(UpdatePasswordRequest $request)
{
// Log out of other sessions

View file

@ -1,19 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UpdateSaveAliasLastUsedRequest;
class SaveAliasLastUsedController extends Controller
{
public function update(UpdateSaveAliasLastUsedRequest $request)
{
if ($request->save_alias_last_used) {
user()->update(['save_alias_last_used' => true]);
} else {
user()->update(['save_alias_last_used' => false]);
}
return back()->with(['flash' => $request->save_alias_last_used ? 'Save Alias Last Used At Enabled Successfully' : 'Save Alias Last Used At Disabled Successfully']);
}
}

View file

@ -11,21 +11,14 @@ use LaravelWebauthn\Facades\Webauthn;
class SettingController extends Controller
{
public function __construct()
{
$this->middleware('throttle:3,1')->only('destroy');
}
public function show()
{
return Inertia::render('Settings/General', [
'defaultAliasDomain' => user()->default_alias_domain,
'defaultAliasFormat' => user()->default_alias_format,
'loginRedirect' => user()->login_redirect->value,
'displayFromFormat' => user()->display_from_format->value,
'useReplyTo' => user()->use_reply_to,
'storeFailedDeliveries' => user()->store_failed_deliveries,
'saveAliasLastUsed' => user()->save_alias_last_used,
'fromName' => user()->from_name ?? '',
'emailSubject' => user()->email_subject ?? '',
'bannerLocation' => user()->banner_location,

View file

@ -54,11 +54,6 @@ class ShowAliasController extends Controller
'emails_blocked',
'emails_replied',
'emails_sent',
'last_forwarded',
'last_blocked',
'last_replied',
'last_sent',
'last_used',
'active',
'created_at',
'updated_at',
@ -70,11 +65,6 @@ class ShowAliasController extends Controller
'-emails_blocked',
'-emails_replied',
'-emails_sent',
'-last_forwarded',
'-last_blocked',
'-last_replied',
'-last_sent',
'-last_used',
'-active',
'-created_at',
'-updated_at',
@ -95,21 +85,16 @@ class ShowAliasController extends Controller
],
]);
$sort = $request->session()->get('aliasesSort', 'created_at');
$direction = $request->session()->get('aliasesSortDirection', 'desc');
$compareOperator = $request->session()->get('aliasesSortCompareOperator', '>');
if ($request->has('sort')) {
$direction = strpos($request->input('sort'), '-') === 0 ? 'desc' : 'asc';
$sort = ltrim($request->input('sort'), '-');
$compareOperator = $direction === 'desc' ? '>' : '<';
$request->session()->put('aliasesSort', $sort);
$request->session()->put('aliasesSortDirection', $direction);
} else {
$direction = 'desc';
$sort = 'created_at';
}
$aliases = user()->aliases()
->select(['id', 'user_id', 'aliasable_id', 'aliasable_type', 'local_part', 'extension', 'email', 'domain', 'description', 'active', 'emails_forwarded', 'emails_blocked', 'emails_replied', 'emails_sent', 'last_forwarded', 'last_blocked', 'last_replied', 'last_sent', 'created_at', 'deleted_at'])
->select(['id', 'user_id', 'aliasable_id', 'aliasable_type', 'local_part', 'extension', 'email', 'domain', 'description', 'active', 'emails_forwarded', 'emails_blocked', 'emails_replied', 'emails_sent', 'created_at', 'deleted_at'])
->when($request->input('recipient'), function ($query, $id) {
return $query->usesRecipientWithId($id, $id === user()->default_recipient_id);
})
@ -119,32 +104,11 @@ class ShowAliasController extends Controller
->when($request->input('username'), function ($query, $id) {
return $query->belongsToAliasable('App\Models\Username', $id);
})
->when($sort !== 'created_at' || $direction !== 'desc', function ($query) use ($sort, $direction, $compareOperator) {
->when($request->input('sort'), function ($query) use ($sort, $direction) {
if ($sort === 'created_at') {
return $query->orderBy($sort, $direction);
}
// If sort is last_used then order by all and return
if ($sort === 'last_used') {
return $query
->orderByRaw(
"CASE
WHEN (last_forwarded {$compareOperator} last_replied
OR (last_forwarded IS NOT NULL
AND last_replied IS NULL))
AND (last_forwarded {$compareOperator} last_sent
OR (last_forwarded IS NOT NULL
AND last_sent IS NULL))
THEN last_forwarded
WHEN last_replied {$compareOperator} last_sent
OR (last_replied IS NOT NULL
AND last_sent IS NULL)
THEN last_replied
ELSE last_sent
END {$direction}"
)->orderBy('created_at', 'desc');
}
// Secondary order by latest first
return $query
->orderBy($sort, $direction)

View file

@ -47,7 +47,7 @@ class ShowDomainController extends Controller
$domain = user()->domains()->findOrFail($id);
return Inertia::render('Domains/Edit', [
'initialDomain' => $domain->only(['id', 'user_id', 'domain', 'description', 'from_name', 'domain_sending_verified_at', 'domain_mx_validated_at', 'auto_create_regex', 'updated_at']),
'initialDomain' => $domain->only(['id', 'user_id', 'domain', 'description', 'from_name', 'domain_sending_verified_at', 'domain_mx_validated_at', 'updated_at']),
]);
}
}

View file

@ -22,7 +22,6 @@ class ShowRuleController extends Controller
})
->orderBy('order')
->get(),
'recipientOptions' => user()->verifiedRecipients()->select(['id', 'email'])->get(),
'search' => $validated['search'] ?? null,
]);
}

View file

@ -44,7 +44,7 @@ class ShowUsernameController extends Controller
$username = user()->usernames()->findOrFail($id);
return Inertia::render('Usernames/Edit', [
'initialUsername' => $username->only(['id', 'user_id', 'username', 'description', 'from_name', 'can_login', 'auto_create_regex', 'updated_at']),
'initialUsername' => $username->only(['id', 'user_id', 'username', 'description', 'from_name', 'can_login', 'updated_at']),
]);
}
}

View file

@ -1,31 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\TestAutoCreateRegexRequest;
class TestAutoCreateRegexController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('throttle:60,1');
}
public function index(TestAutoCreateRegexRequest $request)
{
$query = $request->resource === 'username' ? user()->usernames() : user()->domains();
return response()->json([
'success' => $query
->where('id', $request->id)
->whereNotNull('auto_create_regex')
->whereRaw('? REGEXP auto_create_regex', [$request->local_part])
->exists(),
]);
}
}

71
app/Http/Kernel.php Normal file
View file

@ -0,0 +1,71 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleInertiaRequests::class, // Must be the last item!
],
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array
*/
protected $middlewareAliases = [
'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,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'2fa' => \App\Http\Middleware\VerifyTwoFactorAuth::class,
'webauthn' => \App\Http\Middleware\VerifyWebauthn::class,
];
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -57,7 +57,6 @@ class HandleInertiaRequests extends Middleware
})->all();
},
'version' => GitVersionHelper::version(),
'updateAvailable' => GitVersionHelper::updateAvailable(),
]);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
];
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
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

@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* @var bool
*/
protected $addHttpCookie = true;
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'api/auth/login',
];
}

View file

@ -24,27 +24,9 @@ class ApiAuthenticationLoginRequest extends FormRequest
public function rules()
{
return [
'username' => [
'required',
'regex:/^[a-zA-Z0-9]*$/',
'min:1',
'max:20',
],
'password' => [
'required',
'string',
],
'device_name' => [
'required',
'string',
'max:50',
],
'expiration' => [
'nullable',
'string',
'max:5',
'in:day,week,month,year',
],
'username' => 'required|string',
'password' => 'required|string',
'device_name' => 'required|string|max:50',
];
}
}

View file

@ -26,11 +26,10 @@ class EditDefaultRecipientRequest extends FormRequest
{
return [
'email' => [
'bail',
'required',
'email:rfc,dns',
'max:254',
new RegisterUniqueRecipient,
new RegisterUniqueRecipient(),
'not_in:'.$this->user()->email,
],
'current' => 'required|string|current_password',

View file

@ -1,40 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
class GeneralAliasBulkRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'ids' => Arr::whereNotNull($this->ids ?? []),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
];
}
}

View file

@ -71,11 +71,6 @@ class IndexAliasRequest extends FormRequest
'emails_blocked',
'emails_replied',
'emails_sent',
'last_forwarded',
'last_blocked',
'last_replied',
'last_sent',
'last_used',
'active',
'created_at',
'updated_at',
@ -87,29 +82,12 @@ class IndexAliasRequest extends FormRequest
'-emails_blocked',
'-emails_replied',
'-emails_sent',
'-last_forwarded',
'-last_blocked',
'-last_replied',
'-last_sent',
'-last_used',
'-active',
'-created_at',
'-updated_at',
'-deleted_at',
]),
],
'recipient' => [
'nullable',
'uuid',
],
'domain' => [
'nullable',
'uuid',
],
'username' => [
'nullable',
'uuid',
],
];
}
}

View file

@ -1,47 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\VerifiedRecipientId;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
class RecipientsAliasBulkRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'ids' => Arr::whereNotNull($this->ids ?? []),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
'recipient_ids' => [
'array',
'max:10',
new VerifiedRecipientId,
],
'recipient_ids.*' => 'required|uuid|distinct',
];
}
}

View file

@ -26,10 +26,9 @@ class StoreAliasRecipientRequest extends FormRequest
{
return [
'recipient_ids' => [
'bail',
'array',
'max:10',
new VerifiedRecipientId,
new VerifiedRecipientId(),
],
];
}

View file

@ -47,11 +47,10 @@ class StoreAliasRequest extends FormRequest
'description' => 'nullable|max:200',
'format' => 'nullable|in:random_characters,uuid,random_words,custom',
'recipient_ids' => [
'bail',
'nullable',
'array',
'max:10',
new VerifiedRecipientId,
new VerifiedRecipientId(),
],
];
}
@ -59,13 +58,12 @@ class StoreAliasRequest extends FormRequest
public function withValidator($validator)
{
$validator->sometimes('local_part_without_extension', [
'bail',
'required',
'max:50',
Rule::unique('aliases', 'local_part')->where(function ($query) {
return $query->where('domain', $this->validationData()['domain']);
}),
new ValidAliasLocalPart,
new ValidAliasLocalPart(),
], function () {
$format = $this->validationData()['format'] ?? 'random_characters';

View file

@ -28,14 +28,13 @@ class StoreDomainRequest extends FormRequest
{
return [
'domain' => [
'bail',
'required',
'string',
'max:100',
'max:50',
'unique:domains',
new ValidDomain,
new NotLocalDomain,
new NotUsedAsRecipientDomain,
new ValidDomain(),
new NotLocalDomain(),
new NotUsedAsRecipientDomain(),
],
];
}

View file

@ -27,13 +27,12 @@ class StoreRecipientRequest extends FormRequest
{
return [
'email' => [
'bail',
'required',
'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

@ -2,7 +2,6 @@
namespace App\Http\Requests;
use App\Rules\ValidRegex;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@ -42,10 +41,10 @@ class StoreRuleRequest extends FormRequest
'subject',
'sender',
'alias',
'alias_description',
]),
],
'conditions.*.match' => [
'sometimes',
'required',
Rule::in([
'is exactly',
@ -64,16 +63,9 @@ class StoreRuleRequest extends FormRequest
'min:1',
'max:10',
],
'conditions.*.values.*' => Rule::forEach(function ($value, $attribute, $data) {
if (in_array(array_values($data)[1], ['matches regex', 'does not match regex'])) {
return [
new ValidRegex,
'distinct',
];
}
return ['distinct'];
}),
'conditions.*.values.*' => [
'distinct',
],
'actions' => [
'required',
'array',
@ -87,21 +79,13 @@ class StoreRuleRequest extends FormRequest
'encryption',
'banner',
'block',
'removeAttachments',
'forwardTo',
//'webhook',
'webhook',
]),
],
'actions.*.value' => Rule::forEach(function ($value, $attribute, $data, $action) {
if ($action['type'] === 'forwardTo') {
return [Rule::in(user()->verifiedRecipients()->pluck('id')->toArray())]; // Must be a valid verified recipient
}
return [
'required',
'max:50',
];
}),
'actions.*.value' => [
'required',
'max:50',
],
'operator' => [
'required',
'in:AND,OR',

View file

@ -27,13 +27,12 @@ class StoreUsernameRequest extends FormRequest
{
return [
'username' => [
'bail',
'required',
'regex:/^[a-zA-Z0-9]*$/',
'max:20',
'unique:usernames,username',
new NotBlacklisted,
new NotDeletedUsername,
new NotBlacklisted(),
new NotDeletedUsername(),
],
];
}

View file

@ -1,40 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\ValidAliasLocalPart;
use Illuminate\Foundation\Http\FormRequest;
class TestAutoCreateRegexRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'resource' => [
'required',
'in:username,domain',
],
'id' => [
'required',
'uuid',
],
'local_part' => [
'required',
new ValidAliasLocalPart,
],
];
}
}

View file

@ -2,7 +2,6 @@
namespace App\Http\Requests;
use App\Rules\ValidRegex;
use Illuminate\Foundation\Http\FormRequest;
class UpdateDomainRequest extends FormRequest
@ -27,12 +26,6 @@ class UpdateDomainRequest extends FormRequest
return [
'description' => 'nullable|max:200',
'from_name' => 'nullable|string|max:50',
'auto_create_regex' => [
'nullable',
'string',
'max:100',
new ValidRegex,
],
];
}
}

View file

@ -1,34 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Enums\LoginRedirect;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateLoginRedirectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'redirect' => [
'required',
'integer',
Rule::in(array_column(LoginRedirect::cases(), 'value')),
],
];
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateSaveAliasLastUsedRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'save_alias_last_used' => 'required|boolean',
];
}
}

View file

@ -2,7 +2,6 @@
namespace App\Http\Requests;
use App\Rules\ValidRegex;
use Illuminate\Foundation\Http\FormRequest;
class UpdateUsernameRequest extends FormRequest
@ -27,12 +26,6 @@ class UpdateUsernameRequest extends FormRequest
return [
'description' => 'nullable|max:200',
'from_name' => 'nullable|string|max:50',
'auto_create_regex' => [
'nullable',
'string',
'max:100',
new ValidRegex,
],
];
}
}

View file

@ -25,10 +25,6 @@ class AliasResource extends JsonResource
'emails_replied' => $this->emails_replied,
'emails_sent' => $this->emails_sent,
'recipients' => RecipientResource::collection($this->whenLoaded('recipients')),
'last_forwarded' => $this->last_forwarded?->toDateTimeString(),
'last_blocked' => $this->last_blocked?->toDateTimeString(),
'last_replied' => $this->last_replied?->toDateTimeString(),
'last_sent' => $this->last_sent?->toDateTimeString(),
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),
'deleted_at' => $this->deleted_at?->toDateTimeString(),

View file

@ -14,12 +14,10 @@ class DomainResource extends JsonResource
'domain' => $this->domain,
'description' => $this->description,
'from_name' => $this->from_name,
'aliases' => [],
'aliases_count' => $this->whenCounted('aliases_count'),
'aliases' => AliasResource::collection($this->whenLoaded('aliases')),
'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
'active' => $this->active,
'catch_all' => $this->catch_all,
'auto_create_regex' => $this->auto_create_regex,
'domain_verified_at' => $this->domain_verified_at?->toDateTimeString(),
'domain_mx_validated_at' => $this->domain_mx_validated_at?->toDateTimeString(),
'domain_sending_verified_at' => $this->domain_sending_verified_at?->toDateTimeString(),

View file

@ -12,7 +12,7 @@ class PersonalAccessTokenResource extends JsonResource
'id' => $this->id,
'user_id' => $this->tokenable_id,
'name' => $this->name,
//'abilities' => $this->abilities, // Not selected from controllers
'abilities' => $this->abilities,
'last_used_at' => $this->last_used_at?->toDateTimeString(),
'expires_at' => $this->expires_at?->toDateTimeString(),
'created_at' => $this->created_at?->toDateTimeString(),

View file

@ -18,8 +18,7 @@ class RecipientResource extends JsonResource
'protected_headers' => $this->protected_headers,
'fingerprint' => $this->fingerprint,
'email_verified_at' => $this->email_verified_at?->toDateTimeString(),
'aliases' => [],
'aliases_count' => $this->whenCounted('aliases_count'),
'aliases' => AliasResource::collection($this->whenLoaded('aliases')),
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),
];

View file

@ -20,8 +20,6 @@ class RuleResource extends JsonResource
'replies' => $this->replies,
'sends' => $this->sends,
'active' => $this->active,
'applied' => $this->applied,
'last_applied' => $this->last_applied?->toDateTimeString(),
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),
];

View file

@ -29,9 +29,7 @@ class UserResource extends JsonResource
'email_subject' => $this->email_subject,
'banner_location' => $this->banner_location,
'bandwidth' => $this->bandwidth,
'bandwidth_limit' => $this->getBandwidthLimit(),
'username_count' => $this->usernames()->count(),
'username_limit' => config('anonaddy.additional_username_limit'),
'username_count' => $this->username_count,
'default_username_id' => $this->default_username_id,
'default_recipient_id' => $this->default_recipient_id,
'default_alias_domain' => $this->default_alias_domain,

View file

@ -14,12 +14,10 @@ class UsernameResource extends JsonResource
'username' => $this->username,
'description' => $this->description,
'from_name' => $this->from_name,
'aliases' => [],
'aliases_count' => $this->whenCounted('aliases_count'),
'aliases' => AliasResource::collection($this->whenLoaded('aliases')),
'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
'active' => $this->active,
'catch_all' => $this->catch_all,
'auto_create_regex' => $this->auto_create_regex,
'can_login' => $this->can_login,
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),

View file

@ -158,7 +158,7 @@ class AliasesImport implements ShouldQueue, SkipsEmptyRows, SkipsOnError, SkipsO
'required',
'max:64',
'string',
new ValidAliasLocalPart,
new ValidAliasLocalPart(),
],
'domain' => [
'bail',

View file

@ -47,22 +47,13 @@ class DeleteAccount implements ShouldBeEncrypted, ShouldQueue
'emails_blocked' => 0,
'emails_replied' => 0,
'emails_sent' => 0,
'last_forwarded' => null,
'last_blocked' => null,
'last_replied' => null,
'last_sent' => null,
'active' => false,
'deleted_at' => now(), // Soft delete any aliases at shared domains
]);
// Soft delete any aliases at shared domains
$sharedDomainAliases->delete();
// Force delete any other aliases
$this->user->aliases()->withTrashed()->whereNotIn('domain', config('anonaddy.all_domains'))->forceDelete();
$this->user->recipients()->get()->each(function ($recipient) {
// In order to fire deleting model event. With user to prevent lazy loading.
$recipient->setRelation('user', $this->user);
$recipient->delete();
});
$this->user->recipients()->get()->each->delete(); // In order to fire deleting model event.
$this->user->domains()->delete();
$this->user->usernames()->get()->each->delete(); // In order to fire deleting model event.
$this->user->tokens()->delete();

View file

@ -27,7 +27,7 @@ class SendIncorrectOtpNotification
// Log in auth.log
Log::channel('auth')->info('Failed OTP Notification sent: '.$user->username);
$user->notify(new IncorrectOtpNotification);
$user->notify(new IncorrectOtpNotification());
}
}
}

View file

@ -17,7 +17,6 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Symfony\Component\Mime\Email;
use Throwable;
class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
{
@ -33,10 +32,6 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $sender;
protected $ccs;
protected $tos;
protected $originalCc;
protected $originalTo;
@ -69,8 +64,6 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $encryptedParts;
protected $isInlineEncrypted;
protected $receivedHeaders;
protected $fromEmail;
@ -81,8 +74,6 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $listUnsubscribe;
protected $listUnsubscribePost;
protected $inReplyTo;
protected $references;
@ -111,73 +102,8 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->user = $alias->user;
$this->alias = $alias;
$this->sender = $emailData->sender;
$this->ccs = $emailData->ccs;
$this->tos = $emailData->tos;
$this->originalCc = $emailData->originalCc ?? null;
$this->originalTo = $emailData->originalTo ?? null;
// Create and swap with alias reply-to addresses to allow easy reply-all
if (count($this->ccs)) {
$this->ccs = collect($this->ccs)
->map(function ($cc) {
// Leave alias email Cc as it is
if (stripEmailExtension($cc['address']) === $this->alias->email) {
return [
'display' => $cc['display'] != $cc['address'] ? $cc['display'] : null,
'address' => $this->alias->email,
];
}
return [
'display' => $cc['display'] != $cc['address'] ? $cc['display'] : null,
'address' => $this->alias->local_part.'+'.Str::replaceLast('@', '=', $cc['address']).'@'.$this->alias->domain,
];
})
->filter(fn ($cc) => filter_var($cc['address'], FILTER_VALIDATE_EMAIL))
->map(function ($cc) {
// Only add in display if it exists
if ($cc['display']) {
return $cc['display'].' <'.$cc['address'].'>';
}
return '<'.$cc['address'].'>';
})
->toArray();
}
// Create and swap with alias reply-to addresses to allow easy reply-all
$this->tos = collect($this->tos)
->when(! count($this->tos), function ($tos) {
return $tos->push([
'display' => null,
'address' => $this->alias->email,
]);
})
->map(function ($to) {
// Leave alias email To as it is
if (stripEmailExtension($to['address']) === $this->alias->email) {
return [
'display' => $to['display'] != $to['address'] ? $to['display'] : null,
'address' => $this->alias->email,
];
}
return [
'display' => $to['display'] != $to['address'] ? $to['display'] : null,
'address' => $this->alias->local_part.'+'.Str::replaceLast('@', '=', $to['address']).'@'.$this->alias->domain,
];
})
->filter(fn ($to) => filter_var($to['address'], FILTER_VALIDATE_EMAIL))
->map(function ($to) {
// Only add in display if it exists
if ($to['display']) {
return $to['display'].' <'.$to['address'].'>';
}
return '<'.$to['address'].'>';
})
->toArray();
$this->displayFrom = $emailData->display_from;
$this->replyToAddress = $emailData->reply_to_address ?? $this->sender;
$this->emailSubject = $emailData->subject;
@ -189,7 +115,6 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->size = $emailData->size;
$this->messageId = $emailData->messageId;
$this->listUnsubscribe = $emailData->listUnsubscribe;
$this->listUnsubscribePost = $emailData->listUnsubscribePost;
$this->inReplyTo = $emailData->inReplyTo;
$this->references = $emailData->references;
$this->originalEnvelopeFrom = $emailData->originalEnvelopeFrom;
@ -198,7 +123,6 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->originalSenderHeader = $emailData->originalSenderHeader;
$this->authenticationResults = $emailData->authenticationResults;
$this->encryptedParts = $emailData->encryptedParts ?? null;
$this->isInlineEncrypted = $emailData->isInlineEncrypted ?? false;
$this->receivedHeaders = $emailData->receivedHeaders;
$this->recipientId = $recipient->id;
@ -235,7 +159,13 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
}
}
$displayFrom = $this->getUserDisplayFrom(base64_decode($this->displayFrom));
$displayFrom = base64_decode($this->displayFrom);
if ($displayFrom === $this->sender) {
$displayFrom = Str::replaceLast('@', ' at ', $this->sender);
} else {
$displayFrom = $this->getUserDisplayFrom($displayFrom);
}
$this->email = $this
->from($this->fromEmail, $displayFrom)
@ -245,6 +175,10 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$message->getHeaders()
->addTextHeader('Feedback-ID', 'F:'.$this->alias->id.':anonaddy');
// This header is used to set the To: header as the alias just before sending.
$message->getHeaders()
->addTextHeader('Alias-To', $this->alias->email);
$message->getHeaders()->remove('Message-ID');
if ($this->messageId) {
@ -258,12 +192,6 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
if ($this->listUnsubscribe) {
$message->getHeaders()
->addTextHeader('List-Unsubscribe', base64_decode($this->listUnsubscribe));
// Only check if has original List-Unsubscribe
if ($this->listUnsubscribePost) {
$message->getHeaders()
->addTextHeader('List-Unsubscribe-Post', base64_decode($this->listUnsubscribePost));
}
}
if ($this->inReplyTo) {
@ -401,8 +329,6 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
'shouldBlock' => $this->size === 0,
'needsDkimSignature' => $this->needsDkimSignature(),
'verpDomain' => $this->verpDomain ?? $this->alias->domain,
'ccs' => $this->ccs,
'tos' => $this->tos,
]);
if (isset($replyToEmail)) {
@ -410,11 +336,7 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
}
if ($this->size > 0) {
if ($this->user->save_alias_last_used) {
$this->alias->increment('emails_forwarded', 1, ['last_forwarded' => now()]);
} else {
$this->alias->increment('emails_forwarded');
}
$this->alias->increment('emails_forwarded');
$this->user->bandwidth += $this->size;
$this->user->save();
@ -426,9 +348,10 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
/**
* Handle a job failure.
*
* @param \Throwable $exception
* @return void
*/
public function failed(Throwable $exception)
public function failed()
{
// Send user failed delivery notification, add to failed deliveries table
$recipient = Recipient::find($this->recipientId);
@ -454,7 +377,7 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
'sender' => $this->sender,
'email_type' => 'F',
'status' => null,
'code' => $exception->getMessage(),
'code' => 'An error has occurred, please check the logs.',
'attempted_at' => now(),
]);
}
@ -470,14 +393,13 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
DisplayFromFormat::ADDRESS => str_replace('@', ' at ', $this->sender),
DisplayFromFormat::DOMAINONLY => Str::afterLast($this->sender, '@'),
DisplayFromFormat::NONE => null,
DisplayFromFormat::LEGACY => $displayFrom." '".$this->sender."'",
default => str_replace('@', ' at ', $displayFrom." '".$this->sender."'"),
};
}
private function isAlreadyEncrypted()
{
return $this->encryptedParts || $this->isInlineEncrypted;
return $this->encryptedParts || preg_match('/^-----BEGIN PGP MESSAGE-----([A-Za-z0-9+=\/\n]+)-----END PGP MESSAGE-----$/', base64_decode($this->emailText));
}
private function needsDkimSignature()

View file

@ -15,7 +15,6 @@ use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Symfony\Component\Mime\Email;
use Throwable;
class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
{
@ -31,10 +30,6 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $sender;
protected $ccs;
protected $tos;
protected $emailSubject;
protected $emailText;
@ -69,52 +64,6 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->user = $user;
$this->alias = $alias;
$this->sender = $emailData->sender;
$this->ccs = $emailData->ccs;
$this->tos = $emailData->tos;
// Replace alias reply/send CCs back to proper emails
if (count($this->ccs)) {
$this->ccs = collect($this->ccs)
->map(function ($cc) {
return [
'display' => null,
'address' => Str::replaceLast('=', '@', Str::between($cc['address'], $this->alias->local_part.'+', '@'.$this->alias->domain)),
];
})
->filter(fn ($cc) => filter_var($cc['address'], FILTER_VALIDATE_EMAIL))
->map(function ($cc) {
// Only add in display if it exists
if ($cc['display']) {
return $cc['display'].' <'.$cc['address'].'>';
}
return '<'.$cc['address'].'>';
})
->toArray();
}
// Replace alias reply/send Tos back to proper emails
if (count($this->tos)) {
$this->tos = collect($this->tos)
->map(function ($to) {
return [
'display' => null,
'address' => Str::replaceLast('=', '@', Str::between($to['address'], $this->alias->local_part.'+', '@'.$this->alias->domain)),
];
})
->filter(fn ($to) => filter_var($to['address'], FILTER_VALIDATE_EMAIL))
->map(function ($to) {
// Only add in display if it exists
if ($to['display']) {
return $to['display'].' <'.$to['address'].'>';
}
return '<'.$to['address'].'>';
})
->toArray();
}
$this->emailSubject = $emailData->subject;
$this->emailText = $emailData->text;
$this->emailHtml = $emailData->html;
@ -218,8 +167,6 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
'needsDkimSignature' => $this->needsDkimSignature(),
'aliasDomain' => $this->alias->domain,
'verpDomain' => $this->verpDomain ?? $this->alias->domain,
'ccs' => $this->ccs,
'tos' => $this->tos,
]);
if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
@ -227,11 +174,7 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
}
if ($this->size > 0) {
if ($this->user->save_alias_last_used) {
$this->alias->increment('emails_replied', 1, ['last_replied' => now()]);
} else {
$this->alias->increment('emails_replied');
}
$this->alias->increment('emails_replied');
$this->user->bandwidth += $this->size;
$this->user->save();
@ -243,9 +186,10 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
/**
* Handle a job failure.
*
* @param \Throwable $exception
* @return void
*/
public function failed(Throwable $exception)
public function failed()
{
// Send user failed delivery notification, add to failed deliveries table
$this->user->defaultRecipient->notify(new FailedDeliveryNotification($this->alias->email, $this->sender, base64_decode($this->emailSubject)));
@ -269,7 +213,7 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
'sender' => $this->sender,
'email_type' => 'R',
'status' => null,
'code' => $exception->getMessage(),
'code' => 'An error has occurred, please check the logs.',
'attempted_at' => now(),
]);
}
@ -281,24 +225,14 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
private function removeRealEmailAndTextBanner($text)
{
// Replace <alias+hello=example.com@johndoe.anonaddy.com> with <hello@example.com>
$destination = $this->email->to[0]['address'];
// Reply may be HTML but email client added HTML banner plain text version
return Str::of(str_ireplace($this->sender, '', $text))
->replace($this->alias->local_part.'+'.Str::replaceLast('@', '=', $destination).'@'.$this->alias->domain, $destination)
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '')
->replaceMatches('/(This email was sent to).*?(to deactivate this alias)/mis', '');
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '');
}
private function removeRealEmailAndHtmlBanner($html)
{
// Replace <alias+hello=example.com@johndoe.anonaddy.com> with <hello@example.com>
$destination = $this->email->to[0]['address'];
// Reply may be HTML but have a plain text banner
return Str::of(str_ireplace($this->sender, '', $html))
->replace($this->alias->local_part.'+'.Str::replaceLast('@', '=', $destination).'@'.$this->alias->domain, $destination)
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '')
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/')."(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mi", '');
}

View file

@ -15,7 +15,6 @@ use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Symfony\Component\Mime\Email;
use Throwable;
class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
{
@ -31,10 +30,6 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
protected $sender;
protected $ccs;
protected $tos;
protected $emailSubject;
protected $emailText;
@ -65,52 +60,6 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
$this->user = $user;
$this->alias = $alias;
$this->sender = $emailData->sender;
$this->ccs = $emailData->ccs;
$this->tos = $emailData->tos;
// Replace alias reply/send CCs back to proper emails
if (count($this->ccs)) {
$this->ccs = collect($this->ccs)
->map(function ($cc) {
return [
'display' => null,
'address' => Str::replaceLast('=', '@', Str::between($cc['address'], $this->alias->local_part.'+', '@'.$this->alias->domain)),
];
})
->filter(fn ($cc) => filter_var($cc['address'], FILTER_VALIDATE_EMAIL))
->map(function ($cc) {
// Only add in display if it exists
if ($cc['display']) {
return $cc['display'].' <'.$cc['address'].'>';
}
return '<'.$cc['address'].'>';
})
->toArray();
}
// Replace alias reply/send Tos back to proper emails
if (count($this->tos)) {
$this->tos = collect($this->tos)
->map(function ($to) {
return [
'display' => null,
'address' => Str::replaceLast('=', '@', Str::between($to['address'], $this->alias->local_part.'+', '@'.$this->alias->domain)),
];
})
->filter(fn ($to) => filter_var($to['address'], FILTER_VALIDATE_EMAIL))
->map(function ($to) {
// Only add in display if it exists
if ($to['display']) {
return $to['display'].' <'.$to['address'].'>';
}
return '<'.$to['address'].'>';
})
->toArray();
}
$this->emailSubject = $emailData->subject;
$this->emailText = $emailData->text;
$this->emailHtml = $emailData->html;
@ -202,8 +151,6 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
'needsDkimSignature' => $this->needsDkimSignature(),
'aliasDomain' => $this->alias->domain,
'verpDomain' => $this->verpDomain ?? $this->alias->domain,
'ccs' => $this->ccs,
'tos' => $this->tos,
]);
if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
@ -211,11 +158,7 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
}
if ($this->size > 0) {
if ($this->user->save_alias_last_used) {
$this->alias->increment('emails_sent', 1, ['last_sent' => now()]);
} else {
$this->alias->increment('emails_sent');
}
$this->alias->increment('emails_sent');
$this->user->bandwidth += $this->size;
$this->user->save();
@ -227,9 +170,10 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
/**
* Handle a job failure.
*
* @param \Throwable $exception
* @return void
*/
public function failed(Throwable $exception)
public function failed()
{
// Send user failed delivery notification, add to failed deliveries table
$this->user->defaultRecipient->notify(new FailedDeliveryNotification($this->alias->email, $this->sender, base64_decode($this->emailSubject)));
@ -253,7 +197,7 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
'sender' => $this->sender,
'email_type' => 'S',
'status' => null,
'code' => $exception->getMessage(),
'code' => 'An error has occurred, please check the logs.',
'attempted_at' => now(),
]);
}
@ -266,16 +210,15 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
private function removeRealEmailAndTextBanner($text)
{
return Str::of(str_ireplace($this->sender, '', $text))
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mis', '')
->replaceMatches('/(This email was sent to).*?(to deactivate this alias)/mis', '');
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '');
}
private function removeRealEmailAndHtmlBanner($html)
{
// Reply may be HTML but have a plain text banner
return Str::of(str_ireplace($this->sender, '', $html))
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mis', '')
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/').'(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mis', '');
->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '')
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/')."(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mi", '');
}
/**

View file

@ -19,8 +19,6 @@ class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQue
protected $recipient;
protected $token;
/**
* Create a new message instance.
*
@ -30,7 +28,6 @@ class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQue
{
$this->user = $user;
$this->recipient = $user->defaultRecipient;
$this->token = $user->tokens()->whereDate('expires_at', now()->addWeek())->first();
}
/**
@ -41,14 +38,13 @@ class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQue
public function build()
{
return $this
->subject('Your '.config('app.name').' API key expires soon')
->subject('Your addy.io API key expires soon')
->markdown('mail.token_expiring_soon', [
'user' => $this->user,
'userId' => $this->user->id,
'recipientId' => $this->user->default_recipient_id,
'emailType' => 'TES',
'fingerprint' => $this->recipient->should_encrypt ? $this->recipient->fingerprint : null,
'tokenName' => $this->token?->name,
])
->withSymfonyMessage(function (Email $message) {
$message->getHeaders()

View file

@ -43,11 +43,6 @@ class Alias extends Model
'emails_blocked',
'emails_replied',
'emails_sent',
'last_forwarded',
'last_blocked',
'last_replied',
'last_sent',
'deleted_at',
];
protected $casts = [
@ -56,10 +51,6 @@ class Alias extends Model
'aliasable_id' => 'string',
'aliasable_type' => 'string',
'active' => 'boolean',
'last_forwarded' => 'datetime',
'last_blocked' => 'datetime',
'last_replied' => 'datetime',
'last_sent' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',

View file

@ -10,59 +10,7 @@ class EmailData
{
private static $mimeTypes;
public $sender;
public $display_from;
public $reply_to_address;
public $ccs;
public $tos;
public $originalCc;
public $originalTo;
public $subject;
public $text;
public $html;
public $attachments;
public $inlineAttachments;
public $size;
public $messageId;
public $listUnsubscribe;
public $listUnsubscribePost;
public $inReplyTo;
public $references;
public $originalEnvelopeFrom;
public $originalFromHeader;
public $originalReplyToHeader;
public $originalSenderHeader;
public $authenticationResults;
public $receivedHeaders;
public $encryptedParts;
public $isInlineEncrypted;
public function __construct(Parser $parser, $sender, $size, $emailType = 'F')
public function __construct(Parser $parser, $sender, $size)
{
if (isset($parser->getAddresses('from')[0]['address'])) {
if (filter_var($parser->getAddresses('from')[0]['address'], FILTER_VALIDATE_EMAIL)) {
@ -70,33 +18,14 @@ class EmailData
}
}
// If we can't get a From header then use the envelope from
// If we can't get a From header address then use the envelope from
if (! isset($this->sender)) {
$this->sender = $sender;
}
if (isset($parser->getAddresses('from')[0]['display'])) {
$this->display_from = base64_encode($parser->getAddresses('from')[0]['display']);
} else {
$this->display_from = '';
}
$this->display_from = base64_encode($parser->getAddresses('from')[0]['display']);
if (isset($parser->getAddresses('reply-to')[0])) {
if (filter_var($parser->getAddresses('reply-to')[0]['address'], FILTER_VALIDATE_EMAIL)) {
$this->reply_to_address = $parser->getAddresses('reply-to')[0]['address'];
}
}
try {
$this->ccs = collect($parser->getAddresses('cc'))->all();
} catch (\Throwable $e) {
$this->ccs = [];
}
try {
$this->tos = collect($parser->getAddresses('to'))->all();
} catch (\Throwable $e) {
$this->tos = [];
$this->reply_to_address = $parser->getAddresses('reply-to')[0]['address'];
}
if ($originalCc = $parser->getHeader('cc')) {
@ -115,7 +44,6 @@ class EmailData
$this->size = $size;
$this->messageId = base64_encode(Str::remove(['<', '>'], $parser->getHeader('Message-ID')));
$this->listUnsubscribe = base64_encode($parser->getHeader('List-Unsubscribe'));
$this->listUnsubscribePost = base64_encode($parser->getHeader('List-Unsubscribe-Post'));
$this->inReplyTo = base64_encode($parser->getHeader('In-Reply-To'));
$this->references = base64_encode($parser->getHeader('References'));
$this->originalEnvelopeFrom = $sender;
@ -125,130 +53,40 @@ class EmailData
$this->authenticationResults = $parser->getHeader('X-AnonAddy-Authentication-Results');
$this->receivedHeaders = $parser->getRawHeader('Received');
$isReplyOrSend = in_array($emailType, ['R', 'S']);
if ($parser->getParts()[1]['content-type'] === 'multipart/encrypted') {
$this->encryptedParts = $parser->getAttachments();
// Only try to decrypt Replies or Sends from aliases
if ($isReplyOrSend && config('anonaddy.signing_key_fingerprint')) {
// Check if encrypted with addy.io public key and needs decrypting
$part = collect($this->encryptedParts)->filter(function ($part) {
return $part->getContentType() === 'application/octet-stream';
})->first();
if ($part) {
$this->attemptToDecrypt($part);
}
}
} else {
// If this is a reply or send from an alias then remove any public keys
$this->addAttachments($parser, $isReplyOrSend, $isReplyOrSend);
}
foreach ($parser->getAttachments() as $attachment) {
// Fix incorrect Content Types e.g. 'png', 'pdf', '.pdf', 'text'
$contentType = $attachment->getContentType();
if (preg_match('/^-----BEGIN PGP MESSAGE-----([A-Za-z0-9+=\/\n]+)-----END PGP MESSAGE-----$/', $parser->getMessageBody('text'))) {
$this->isInlineEncrypted = true;
if ($isReplyOrSend && config('anonaddy.signing_key_fingerprint')) {
$this->attemptToDecryptInline($parser->getMessageBody('text'));
}
}
}
private function addAttachments(Parser $parser, $removePublicKeys = false, $removeSignature = false)
{
foreach ($parser->getAttachments() as $attachment) {
// Fix incorrect Content Types e.g. 'png', 'pdf', '.pdf', 'text'
$contentType = $attachment->getContentType();
if ($removePublicKeys && $contentType === 'application/pgp-keys') {
continue;
}
if ($removeSignature && $contentType === 'application/pgp-signature') {
continue;
}
if ($contentType === 'text') {
$this->text = base64_encode(stream_get_contents($attachment->getStream()));
} else {
if (! str_contains($contentType, '/')) {
if (self::$mimeTypes === null) {
self::$mimeTypes = new MimeTypes;
}
$contentType = self::$mimeTypes->getMimeTypes($contentType)[0] ?? 'application/octet-stream';
}
if ($attachment->getContentDisposition() === 'inline') {
$this->inlineAttachments[] = [
'stream' => base64_encode(stream_get_contents($attachment->getStream())),
'file_name' => base64_encode($attachment->getFileName()),
'mime' => base64_encode($contentType),
'contentDisposition' => base64_encode($attachment->getContentDisposition()),
'contentId' => base64_encode($attachment->getContentID()),
];
if ($contentType === 'text') {
$this->text = base64_encode(stream_get_contents($attachment->getStream()));
} else {
$this->attachments[] = [
'stream' => base64_encode(stream_get_contents($attachment->getStream())),
'file_name' => base64_encode($attachment->getFileName()),
'mime' => base64_encode($contentType),
];
if (! str_contains($contentType, '/')) {
if (self::$mimeTypes === null) {
self::$mimeTypes = new MimeTypes();
}
$contentType = self::$mimeTypes->getMimeTypes($contentType)[0] ?? 'application/octet-stream';
}
if ($attachment->getContentDisposition() === 'inline') {
$this->inlineAttachments[] = [
'stream' => base64_encode(stream_get_contents($attachment->getStream())),
'file_name' => base64_encode($attachment->getFileName()),
'mime' => base64_encode($contentType),
'contentDisposition' => base64_encode($attachment->getContentDisposition()),
'contentId' => base64_encode($attachment->getContentID()),
];
} else {
$this->attachments[] = [
'stream' => base64_encode(stream_get_contents($attachment->getStream())),
'file_name' => base64_encode($attachment->getFileName()),
'mime' => base64_encode($contentType),
];
}
}
}
}
}
private function attemptToDecrypt($part)
{
try {
$gnupg = new \gnupg;
$gnupg->cleardecryptkeys();
$gnupg->adddecryptkey(config('anonaddy.signing_key_fingerprint'), null);
$encrypted = stream_get_contents($part->getStream());
$decrypted = $gnupg->decrypt($encrypted);
if ($decrypted) {
$decryptedParser = new Parser;
$decryptedParser->setText($decrypted);
// Set decrypted data as subject (as may have encrypted subject too), html and text
$this->subject = base64_encode($decryptedParser->getHeader('subject'));
$this->text = base64_encode($decryptedParser->getMessageBody('text'));
$this->html = base64_encode($decryptedParser->getMessageBody('html'));
// Add attachments
$this->addAttachments($decryptedParser, true, true);
// Set encrypted parts to NULL
$this->encryptedParts = null;
}
} catch (\Exception $e) {
report($e);
}
}
private function attemptToDecryptInline($text)
{
try {
$gnupg = new \gnupg;
$gnupg->cleardecryptkeys();
$gnupg->adddecryptkey(config('anonaddy.signing_key_fingerprint'), null);
$decrypted = $gnupg->decrypt($text);
if ($decrypted) {
// Set decrypted text as message text
$this->text = base64_encode($decrypted);
$this->html = null;
}
} catch (\Exception $e) {
report($e);
}
}
}

View file

@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class FailedDelivery extends Model
{
@ -108,20 +107,6 @@ class FailedDelivery extends Model
);
}
protected function status(): Attribute
{
return Attribute::make(
set: fn (?string $value) => Str::ascii($value),
);
}
protected function code(): Attribute
{
return Attribute::make(
set: fn (?string $value) => Str::ascii($value),
);
}
/**
* Get the user for the failed delivery.
*/

View file

@ -205,7 +205,7 @@ class Recipient extends Model
*/
public function sendEmailVerificationNotification()
{
$this->notify(new CustomVerifyEmail);
$this->notify(new CustomVerifyEmail());
}
/**
@ -215,7 +215,7 @@ class Recipient extends Model
*/
public function sendUsernameReminderNotification()
{
$this->notify(new UsernameReminder);
$this->notify(new UsernameReminder());
}
/**

View file

@ -24,8 +24,6 @@ class Rule extends Model
'replies',
'sends',
'active',
'applied',
'last_applied',
'order',
];
@ -38,8 +36,6 @@ class Rule extends Model
'sends' => 'boolean',
'conditions' => 'array',
'actions' => 'array',
'applied' => 'integer',
'last_applied' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];

View file

@ -3,7 +3,6 @@
namespace App\Models;
use App\Enums\DisplayFromFormat;
use App\Enums\LoginRedirect;
use App\Notifications\CustomResetPassword;
use App\Notifications\CustomVerifyEmail;
use App\Traits\HasEncryptedAttributes;
@ -14,7 +13,6 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens;
@ -42,7 +40,6 @@ class User extends Authenticatable implements MustVerifyEmail
'email_subject',
'banner_location',
'display_from_format',
'login_redirect',
'catch_all',
'bandwidth',
'reject_until',
@ -52,7 +49,6 @@ class User extends Authenticatable implements MustVerifyEmail
'default_alias_format',
'use_reply_to',
'store_failed_deliveries',
'save_alias_last_used',
'default_username_id',
'default_recipient_id',
'password',
@ -92,7 +88,6 @@ class User extends Authenticatable implements MustVerifyEmail
'two_factor_enabled' => 'boolean',
'use_reply_to' => 'boolean',
'store_failed_deliveries' => 'boolean',
'save_alias_last_used' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'email_verified_at' => 'datetime',
@ -100,7 +95,6 @@ class User extends Authenticatable implements MustVerifyEmail
'defer_until' => 'datetime',
'defer_new_aliases_until' => 'datetime',
'display_from_format' => DisplayFromFormat::class,
'login_redirect' => LoginRedirect::class,
];
/**
@ -174,10 +168,8 @@ class User extends Authenticatable implements MustVerifyEmail
*/
protected function defaultAliasDomain(): Attribute
{
$defaultDomain = $this->canCreateSharedDomainAliases() ? config('anonaddy.domain') : $this->username.'.'.config('anonaddy.domain');
return Attribute::make(
get: fn (?string $value) => $value ?? $defaultDomain,
get: fn (?string $value) => $value ?? config('anonaddy.domain'),
);
}
@ -414,7 +406,7 @@ class User extends Authenticatable implements MustVerifyEmail
*/
public function sendEmailVerificationNotification()
{
$this->notify(new CustomVerifyEmail);
$this->notify(new CustomVerifyEmail());
}
/**
@ -430,10 +422,6 @@ class User extends Authenticatable implements MustVerifyEmail
public function hasVerifiedDefaultRecipient()
{
if (! isset($this->defaultRecipient->email_verified_at)) {
return false;
}
return ! is_null($this->defaultRecipient->email_verified_at);
}
@ -503,7 +491,7 @@ class User extends Authenticatable implements MustVerifyEmail
public function hasReachedUsernameLimit()
{
return $this->usernames()->count() >= config('anonaddy.additional_username_limit');
return $this->username_count >= config('anonaddy.additional_username_limit');
}
public function isVerifiedRecipient($email)
@ -550,7 +538,7 @@ class User extends Authenticatable implements MustVerifyEmail
public function deleteKeyFromKeyring($fingerprint): void
{
$gnupg = new \gnupg;
$gnupg = new \gnupg();
$recipientsUsingFingerprint = $this
->recipients()
@ -565,17 +553,14 @@ class User extends Authenticatable implements MustVerifyEmail
})
->pluck('email')
->each(function ($email) use ($gnupg, $fingerprint, $recipientsUsingFingerprint) {
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
if ($this->isVerifiedRecipient($email) && $recipientsUsingFingerprint->count() === 1) {
$gnupg->deletekey($fingerprint);
if ($this->isVerifiedRecipient($email) && $recipientsUsingFingerprint->count() === 1) {
$gnupg->deletekey($fingerprint);
$recipientsUsingFingerprint->first()->update([
'should_encrypt' => false,
'fingerprint' => null,
]);
}
$recipientsUsingFingerprint->first()->update([
'should_encrypt' => false,
'fingerprint' => null,
]);
}
});
}
}
@ -615,29 +600,13 @@ class User extends Authenticatable implements MustVerifyEmail
});
})
->concat($customDomains)
->when($this->canCreateSharedDomainAliases(), function (Collection $collection) use ($allDomains) {
return $collection->concat($allDomains);
})
->concat($allDomains)
->reverse()
->values();
}
public function sharedDomainOptions()
{
if ($this->canCreateSharedDomainAliases()) {
return config('anonaddy.all_domains');
}
return [];
}
public function isAdminUser()
{
return $this->username === config('anonaddy.admin_username');
}
public function canCreateSharedDomainAliases()
{
return config('anonaddy.non_admin_shared_domains') || $this->isAdminUser();
return config('anonaddy.all_domains');
}
}

Some files were not shown because too many files have changed in this diff Show more