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
5
.gitignore
vendored
|
@ -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
|
||||
|
|
65
README.md
65
README.md
|
@ -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)!
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
|
@ -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());
|
||||
})
|
||||
|
|
|
@ -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
48
app/Console/Kernel.php
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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']));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,5 +11,4 @@ enum DisplayFromFormat: int
|
|||
case ADDRESS = 4;
|
||||
case NONE = 5;
|
||||
case DOMAINONLY = 6;
|
||||
case LEGACY = 7;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
27
app/Exceptions/Handler.php
Normal file
27
app/Exceptions/Handler.php
Normal 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
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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()],
|
||||
], [
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ class ShowRuleController extends Controller
|
|||
})
|
||||
->orderBy('order')
|
||||
->get(),
|
||||
'recipientOptions' => user()->verifiedRecipients()->select(['id', 'email'])->get(),
|
||||
'search' => $validated['search'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
71
app/Http/Kernel.php
Normal 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,
|
||||
];
|
||||
}
|
17
app/Http/Middleware/Authenticate.php
Normal file
17
app/Http/Middleware/Authenticate.php
Normal 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');
|
||||
}
|
||||
}
|
17
app/Http/Middleware/EncryptCookies.php
Normal file
17
app/Http/Middleware/EncryptCookies.php
Normal 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 = [
|
||||
//
|
||||
];
|
||||
}
|
|
@ -57,7 +57,6 @@ class HandleInertiaRequests extends Middleware
|
|||
})->all();
|
||||
},
|
||||
'version' => GitVersionHelper::version(),
|
||||
'updateAvailable' => GitVersionHelper::updateAvailable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal 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 = [
|
||||
//
|
||||
];
|
||||
}
|
30
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
30
app/Http/Middleware/RedirectIfAuthenticated.php
Normal 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);
|
||||
}
|
||||
}
|
18
app/Http/Middleware/TrimStrings.php
Normal file
18
app/Http/Middleware/TrimStrings.php
Normal 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',
|
||||
];
|
||||
}
|
28
app/Http/Middleware/TrustProxies.php
Normal file
28
app/Http/Middleware/TrustProxies.php
Normal 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;
|
||||
}
|
24
app/Http/Middleware/VerifyCsrfToken.php
Normal file
24
app/Http/Middleware/VerifyCsrfToken.php
Normal 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',
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -26,10 +26,9 @@ class StoreAliasRecipientRequest extends FormRequest
|
|||
{
|
||||
return [
|
||||
'recipient_ids' => [
|
||||
'bail',
|
||||
'array',
|
||||
'max:10',
|
||||
new VerifiedRecipientId,
|
||||
new VerifiedRecipientId(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class StoreReorderRuleRequest extends FormRequest
|
|||
'ids' => [
|
||||
'required',
|
||||
'array',
|
||||
new ValidRuleId,
|
||||
new ValidRuleId(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
];
|
||||
|
|
|
@ -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(),
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -158,7 +158,7 @@ class AliasesImport implements ShouldQueue, SkipsEmptyRows, SkipsOnError, SkipsO
|
|||
'required',
|
||||
'max:64',
|
||||
'string',
|
||||
new ValidAliasLocalPart,
|
||||
new ValidAliasLocalPart(),
|
||||
],
|
||||
'domain' => [
|
||||
'bail',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mi', '')
|
||||
->replaceMatches('/(This email was sent to).*?(to deactivate this alias)/mis', '');
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/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)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mi', '')
|
||||
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/')."(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mi", '');
|
||||
}
|
||||
|
|
|
@ -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)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mis', '')
|
||||
->replaceMatches('/(This email was sent to).*?(to deactivate this alias)/mis', '');
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/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)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mis', '')
|
||||
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/').'(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mis', '');
|
||||
->replaceMatches('/(?s)((<|<)!--banner-info--(>|>)).*?((<|<)!--banner-info--(>|>))/mi', '')
|
||||
->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/')."(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mi", '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue