mirror of
https://github.com/anonaddy/anonaddy.git
synced 2025-12-28 07:55:07 +00:00
Rebrand update
This commit is contained in:
parent
045e82bae8
commit
8d6ddb4434
12
.env.example
12
.env.example
@ -1,4 +1,4 @@
|
||||
APP_NAME=AnonAddy
|
||||
APP_NAME=addy.io
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
@ -11,8 +11,8 @@ LOG_CHANNEL=stack
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=anonaddy_database
|
||||
DB_USERNAME=anonaddy
|
||||
DB_DATABASE=addy_database
|
||||
DB_USERNAME=addy
|
||||
DB_PASSWORD=secret
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
@ -28,9 +28,9 @@ REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# The from name to be used for outgoing email notifications from AnonAddy
|
||||
# The from name to be used for outgoing email notifications from addy.io
|
||||
MAIL_FROM_NAME=Example
|
||||
# The from address to be used for outgoing email notifications from AnonAddy
|
||||
# The from address to be used for outgoing email notifications from addy.io
|
||||
MAIL_FROM_ADDRESS=mailer@example.com
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_HOST=mail.example.com
|
||||
@ -48,7 +48,7 @@ ANONADDY_DOMAIN=example.com
|
||||
ANONADDY_HOSTNAME=mail.example.com
|
||||
ANONADDY_DNS_RESOLVER=127.0.0.1
|
||||
ANONADDY_ALL_DOMAINS=example.com,example2.com
|
||||
# Used for verifying custom domains, can be anything e.g. 64U64QcpgWHAZPyr4nN58kDGvwj9TkKMGyuXcjMFA7CdhTDy2f
|
||||
# Used for verifying custom domains and variable envelope return paths, can be anything e.g. 64U64QcpgWHAZPyr4nN58kDGvwj9TkKMGyuXcjMFA7CdhTDy2f
|
||||
ANONADDY_SECRET=long-random-string
|
||||
# Number of emails that can be forwarded through the service per hour by any one user
|
||||
ANONADDY_LIMIT=200
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@
|
||||
/storage/*.key
|
||||
/storage/debugbar
|
||||
/vendor
|
||||
/postfix/vendor
|
||||
/.idea
|
||||
/.vscode
|
||||
/.vagrant
|
||||
|
||||
80
README.md
80
README.md
@ -1,12 +1,12 @@
|
||||
# Anonymous Email Forwarding
|
||||
|
||||
This is the source code for self-hosting AnonAddy.
|
||||
This is the source code for self-hosting addy.io.
|
||||
|
||||
## FAQ
|
||||
|
||||
- [Why is it called AnonAddy?](#why-is-it-called-anonaddy)
|
||||
- [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 AnonAddy?](#why-should-i-use-anonaddy)
|
||||
- [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)
|
||||
@ -30,7 +30,7 @@ This is the source code for self-hosting AnonAddy.
|
||||
- [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)
|
||||
- [Does AnonAddy strip out the banner information when I reply to an email?](#does-anonaddy-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)
|
||||
@ -48,15 +48,15 @@ This is the source code for self-hosting AnonAddy.
|
||||
- [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 AnonAddy if you die?](#what-happens-to-anonaddy-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 AnonAddy?](#whos-behind-anonaddy)
|
||||
- [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 AnonAddy?
|
||||
## Why is it called addy.io?
|
||||
|
||||
AnonAddy is short for "Anonymous Email Address". The word "Addy" is internet slang for email address, e.g.
|
||||
Addy is short for "Address". The word "Addy" is internet slang for an email address, e.g.
|
||||
|
||||
> "My addy is being spammed. I should've kept it private."
|
||||
|
||||
@ -75,7 +75,7 @@ I made the code open-source to show everyone what was going on behind the scenes
|
||||
|
||||
I use this service myself for the vast majority of sites I'm signed up to.
|
||||
|
||||
## Why should I use AnonAddy?
|
||||
## Why should I use addy.io?
|
||||
|
||||
There are a number of reasons you should consider using this service:
|
||||
|
||||
@ -104,24 +104,24 @@ Yes you can use your own domain name so you can also have *@example.com as your
|
||||
|
||||
## Can I add a domain and also use it as a recipient?
|
||||
|
||||
No, you cannot use the same domain as a custom domain and also for a recipient on AnonAddy.
|
||||
No, you cannot use the same domain as a custom domain and also for a recipient on addy.io.
|
||||
|
||||
e.g if you add "example.com" as a custom domain, you cannot then add "xyz@example.com" as a recipient. This is because a domain cannot direct email to multiple locations simultaneously using MX records. So your email would arrive for "example.com" and then attempt to be forwarded to "xyz@example.com" which would create a loop.
|
||||
|
||||
You can instead use a subdomain for your custom domain, e.g. "mail.example.com" instead of "example.com", this would allow you to create *@mail.example.com for your aliases. More details can be found [here](https://anonaddy.com/help/adding-a-custom-domain/).
|
||||
You can instead use a subdomain for your custom domain, e.g. "mail.example.com" instead of "example.com", this would allow you to create *@mail.example.com for your aliases. More details can be found [here](https://addy.io/help/adding-a-custom-domain/).
|
||||
|
||||
## Can I add a domain if I'm already using it for email somewhere else?
|
||||
|
||||
If you have a custom domain say **example.com** and you are already using it for email somewhere else e.g. ProtonMail or Namecheap then you cannot also use it simultaneously with AnonAddy.
|
||||
If you have a custom domain say **example.com** and you are already using it for email somewhere else e.g. ProtonMail or Namecheap then you cannot also use it simultaneously with addy.io.
|
||||
|
||||
This is because emails cannot be handled by multiple different mail servers at the same time, even if they have the same priority MX records. It can only be delivered to one mail server at a time which will typically be the MX record with the smallest number since this has the highest priority.
|
||||
|
||||
You can either:
|
||||
|
||||
- Migrate your domain to AnonAddy by removing the current provider's MX records and adding AnonAddy's.
|
||||
- Or, if you would like to keep using your domain with your current email provider then I would recommend instead adding a subdomain of it to AnonAddy such as **mail.example.com**.
|
||||
- Migrate your domain to addy.io by removing the current provider's MX records and adding addy.io's.
|
||||
- Or, if you would like to keep using your domain with your current email provider then I would recommend instead adding a subdomain of it to addy.io such as **mail.example.com**.
|
||||
|
||||
Using a subdomain will not interfere with your current email setup and you'll be able to create aliases ***@mail.example.com** through AnonAddy.
|
||||
Using a subdomain will not interfere with your current email setup and you'll be able to create aliases ***@mail.example.com** through addy.io.
|
||||
|
||||
## Why should I use this instead of a similar service?
|
||||
|
||||
@ -134,14 +134,14 @@ Here are a few reasons I can think of:
|
||||
* Open-source application code
|
||||
* No limitation on the number of aliases that can be created
|
||||
* Generous monthly bandwidth
|
||||
* Multiple domains to choose for aliases (currently anonaddy.com, anonaddy.me and another 3 for paid plan users)
|
||||
* Multiple domains to choose for aliases (currently anonaddy.com, anonaddy.me and more for paid plan users)
|
||||
* Ability to generate random character and random word aliases at shared domains
|
||||
* Ability to add additional usernames to compartmentalise aliases
|
||||
* New features added regularly
|
||||
|
||||
## Is there a browser extension?
|
||||
|
||||
Yes there is an [open-source](https://github.com/anonaddy/browser-extension) browser extension available to download for [Firefox](https://addons.mozilla.org/en-GB/firefox/addon/anonaddy/) and [Chrome](https://chrome.google.com/webstore/detail/anonaddy/iadbdpnoknmbdeolbapdackdcogdmjpe) (also available on other chromium based browsers such as Brave and Vivaldi). You can use the extension to generate new aliases remotely.
|
||||
Yes there is an [open-source](https://github.com/anonaddy/browser-extension) browser extension available to download for [Firefox](https://addons.mozilla.org/en-GB/firefox/addon/addy_io/) and [Chrome](https://chrome.google.com/webstore/detail/addyio-anonymous-email-fo/iadbdpnoknmbdeolbapdackdcogdmjpe) (also available on other chromium based browsers such as Brave and Vivaldi). You can use the extension to generate new aliases remotely.
|
||||
|
||||
## Is there an Android app?
|
||||
|
||||
@ -159,7 +159,7 @@ Yes, [http.james'](https://httpjames.space/) [open-source](https://github.com/ra
|
||||
|
||||
## How do I add my own GPG/OpenPGP key for encryption?
|
||||
|
||||
On the recipients page you simply need to click "Add public key" and paste in your **public** key data. Now all emails forwarded to you will be encrypted with your key. You can even hide and encrypt the subject as AnonAddy supports protected headers.
|
||||
On the recipients page you simply need to click "Add public key" and paste in your **public** key data. Now all emails forwarded to you will be encrypted with your key. You can even hide and encrypt the subject as addy.io supports protected headers.
|
||||
|
||||
## Are attachments encrypted too?
|
||||
|
||||
@ -167,11 +167,11 @@ Yes attachments are part of the email body and are also encrypted if you have it
|
||||
|
||||
## Are forwarded emails signed when encryption is enabled?
|
||||
|
||||
Yes when you have encryption enabled all forwarded emails are signed using our mailer@anonaddy.me private key.
|
||||
Yes when you have encryption enabled all forwarded emails are signed using our no-reply@addy.io private key.
|
||||
|
||||
You can add this key to your own keyring so that you can verify emails have come from us.
|
||||
|
||||
The fingerprint of the mailer@anonaddy.me key is "26A987650243B28802524E2F809FD0D502E2F695" you can find the key on [https://keys.openpgp.org](https://keys.openpgp.org/search?q=26A987650243B28802524E2F809FD0D502E2F695).
|
||||
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).
|
||||
|
||||
## What if I don't want anyone to link ownership of my aliases together?
|
||||
|
||||
@ -220,11 +220,11 @@ All you need to do is click reply in your email client or web interface and it w
|
||||
|
||||
To check if a reply has worked properly check in your dashboard if the reply count has been incremented for that alias.
|
||||
|
||||
For further details please see this help article - [Replying to email using an alias](https://anonaddy.com/help/replying-to-email-using-an-alias/).
|
||||
For further details please see this help article - [Replying to email using an alias](https://addy.io/help/replying-to-email-using-an-alias/).
|
||||
|
||||
## I'm trying to reply/send from an alias but the email keeps coming back to me, what's wrong?
|
||||
|
||||
If you are trying to reply or send from an alias but the email keeps coming back to yourself then it is most likely because you are not sending the message from an email address that **is not listed as a verified recipient** on your AnonAddy account.
|
||||
If you are trying to reply or send from an alias but the email keeps coming back to yourself then it is most likely because you are not sending the message from an email address that **is not listed as a verified recipient** on your addy.io account.
|
||||
|
||||
If you try to reply or send from an alias using an unverified email address then the message will simply be forwarded to you as it would be if it was sent by any other sender.
|
||||
|
||||
@ -236,11 +236,11 @@ If you see the rejection message `550 5.1.1 Recipient address rejected: Address
|
||||
|
||||
If you receive an email notification with the subject "Attempted reply/send from alias has failed" then it is usually because you have a verified recipient that is using your own domain which does not have a DMARC policy.
|
||||
|
||||
> Note: This is referring to **your verified recipient address** on your AnonAddy account **and not** any of your custom domains or the email address that you are replying / sending to
|
||||
> Note: This is referring to **your verified recipient address** on your addy.io account **and not** any of your custom domains or the email address that you are replying / sending to
|
||||
|
||||
When replying or sending from an alias, **additional checks** are carried out to ensure it is not a spoofed email. Your AnonAddy recipient's email domain must pass DMARC checks in order to protect against spoofed emails and to make sure that the reply/send from attempt definitely came from your recipient.
|
||||
When replying or sending from an alias, **additional checks** are carried out to ensure it is not a spoofed email. Your addy.io recipient's email domain must pass DMARC checks in order to protect against spoofed emails and to make sure that the reply/send from attempt definitely came from your recipient.
|
||||
|
||||
For example if the verified recipient on your AnonAddy account is `hello@example.com` and you get this email notification then it is because the domain "example.com" does not have a DMARC policy in place.
|
||||
For example if the verified recipient on your addy.io account is `hello@example.com` and you get this email notification then it is because the domain "example.com" does not have a DMARC policy in place.
|
||||
|
||||
To resolve this you simply need to add a DMARC record, for example:
|
||||
|
||||
@ -253,9 +253,9 @@ You should also have SPF and DKIM records in place.
|
||||
|
||||
To learn more about DMARC please see this site - [https://dmarc.org/](https://dmarc.org/).
|
||||
|
||||
If your AnonAddy 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.
|
||||
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.
|
||||
|
||||
## Does AnonAddy 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?
|
||||
|
||||
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.
|
||||
|
||||
@ -287,7 +287,7 @@ If you need to send an email to an address with an extension e.g. **hello+whatev
|
||||
|
||||
Just enter the extension too!
|
||||
|
||||
For further details please see this help article - [Sending email from an alias](https://anonaddy.com/help/sending-email-from-an-alias/).
|
||||
For further details please see this help article - [Sending email from an alias](https://addy.io/help/sending-email-from-an-alias/).
|
||||
|
||||
## Will people see my real email if I reply to a forwarded one?
|
||||
|
||||
@ -309,10 +309,10 @@ A few days before your billing cycle ends you will receive an email letting you
|
||||
|
||||
* Any custom domains will be **deactivated**
|
||||
* Any additional usernames will be **deactivated**
|
||||
* If you have any more than **2 recipients** they will be **deleted**
|
||||
* If you have any more than **1 recipient** they will be **deleted**
|
||||
* Paid account settings will be reverted to default values
|
||||
* Any aliases using paid plan only domains will be **deactivated**
|
||||
* If you have any more than 20 aliases using a shared domain e.g. anonaddy.me they will be **deactivated**
|
||||
* If you have any more than 10 aliases using a shared domain e.g. anonaddy.me they will be **deactivated**
|
||||
* If your account username has catch-all disabled then it will be enabled
|
||||
|
||||
You will not be able to activate any of the above again until you resubscribe.
|
||||
@ -366,7 +366,7 @@ Yes, you can login with any of your usernames. You can add 1 additional username
|
||||
|
||||
## I'm not receiving any emails, what's wrong?
|
||||
|
||||
Please make sure to add mailer@anonaddy.me, mailer@anonaddy.com and any other aliases you use to your address book and also to check your spam folder. Make sure to mark emails from AnonAddy as safe if they turn up in spam.
|
||||
Please make sure to add no-reply@addy.io and any aliases you use to your address book and also to check your spam folder. Make sure to mark emails from addy.io as safe if they turn up in spam.
|
||||
|
||||
If an alias has been deleted and you try to send email to it, the emails will be rejected with an error message - "550 5.1.1 Recipient address rejected: Address does not exist".
|
||||
|
||||
@ -382,7 +382,7 @@ For some reason Apple seems to think these emails are spam/phishing and returns
|
||||
|
||||
If you are having issues with emails being rejected as "possibly spammy" by Google, iCloud or Microsoft then please try the following steps if you can:
|
||||
|
||||
1. **Replace the email subject** by going to your settings in AnonAddy
|
||||
1. **Replace the email subject** by going to your settings in addy.io
|
||||
2. Try adding a GPG key and **enabling encryption**. This will prevent the email's content being scanned and reduce the chance of it being rejected.
|
||||
3. Enable the option to hide and encrypt the email subject
|
||||
4. Try disabling the banner information on forwarded emails
|
||||
@ -406,11 +406,11 @@ Please make sure you are using your account username (e.g. johndoe) and not your
|
||||
|
||||
2. Forgotten password
|
||||
|
||||
If you've forgotten your password you can reset it by entering your username here - https://app.anonaddy.com/password/reset
|
||||
If you've forgotten your password you can reset it by entering your username here - https://app.addy.io/password/reset
|
||||
|
||||
3. Forgotten username
|
||||
|
||||
If you've forgotten your username you can request a reminder by entering your email address here - https://app.anonaddy.com/username/reminder
|
||||
If you've forgotten your username you can request a reminder by entering your email address here - https://app.addy.io/username/reminder
|
||||
|
||||
4. Lost 2FA device
|
||||
|
||||
@ -424,11 +424,11 @@ If you have a YubiKey and are using Windows and have an issue with your personal
|
||||
|
||||
I am very passionate about this project. I use it myself every day and will be keeping it running indefinitely. The service also provides me with an income.
|
||||
|
||||
## What happens to AnonAddy if you die?
|
||||
## What happens to addy.io if you die?
|
||||
|
||||
I do have someone in place who can keep the service running in the event of me not being here. They are able to continue paying for the servers that host AnonAddy and the domains that it uses. All AnonAddy domains also always have over 5 years until they expire.
|
||||
I do have someone in place who can keep the service running in the event of me not being here. They are able to continue paying for the servers that host addy.io and the domains that it uses. All addy.io domains also always have over 5 years until they expire.
|
||||
|
||||
They would make a Twitter announcement informing all users that they would be keeping the service running. You would then be able to decide whether you'd like to continue using AnonAddy or start to update your email addresses.
|
||||
They would make a Twitter announcement informing all users that they would be keeping the service running. You would then be able to decide whether you'd like to continue using addy.io or start to update your email addresses.
|
||||
|
||||
## Is the application tested?
|
||||
|
||||
@ -440,13 +440,13 @@ You will need to set up your own server with Postfix so that you can pipe the re
|
||||
|
||||
For those who prefer using Docker there is an image you can use here - [github.com/anonaddy/docker](https://github.com/anonaddy/docker).
|
||||
|
||||
## Who's behind AnonAddy?
|
||||
## Who's behind addy.io?
|
||||
|
||||
My name is Will Browning, I'm a web developer from the UK and an advocate for online privacy and open-source software. You can find me on [Twitter](https://twitter.com/willbrowningme) although I don't tweet that much!
|
||||
|
||||
## I couldn't find an answer to my question, how can I contact you?
|
||||
|
||||
For any other questions just send an email to - [contact@anonaddy.com](mailto:contact@anonaddy.com) ([GPG Key](https://anonaddy.com/anonaddy-contact-public-key.asc))
|
||||
For any other questions just send an email to - contact (at) help.addy.io ([GPG Key](https://addy.io/contact-public-key.asc))
|
||||
|
||||
## Self Hosting
|
||||
|
||||
@ -470,7 +470,7 @@ For full details please see the [self-hosting instructions file](SELF-HOSTING.md
|
||||
|
||||
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 [AnonAddy Docker image](https://github.com/anonaddy/docker)!
|
||||
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)!
|
||||
|
||||
## Thanks
|
||||
|
||||
|
||||
@ -11,11 +11,11 @@ notify me. I welcome working with you to resolve the issue promptly. Thanks in a
|
||||
degradation of the service. Only interact with accounts you own or with explicit permission of the
|
||||
account holder.
|
||||
- If you would like to encrypt your report, please use the PGP key with fingerprint
|
||||
`5FCAFD8A67D2A783CFF4D0E31AC6D923E6FB4EF7` (available on the openpgp.org keyserver).
|
||||
`E652C2DB43859328F35575DEBF7B93C6497510D0` (available on the openpgp.org keyserver).
|
||||
|
||||
# Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability please send an email to contact@anonaddy.com, you can use the PGP key above if you wish to encrypt it.
|
||||
To report a vulnerability please send an email to contact (at) help.addy.io, you can use the PGP key above if you wish to encrypt it.
|
||||
|
||||
# In-scope
|
||||
|
||||
|
||||
@ -39,7 +39,8 @@ class CheckDomainsMxValidation extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Domain::all()
|
||||
Domain::with('user.defaultUsername')
|
||||
->get()
|
||||
->each(function ($domain) {
|
||||
try {
|
||||
if (! $domain->checkMxRecords()) {
|
||||
|
||||
@ -39,7 +39,8 @@ class CheckDomainsSendingVerification extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Domain::whereNotNull('domain_sending_verified_at')->get()
|
||||
Domain::with('user.defaultUsername')
|
||||
->whereNotNull('domain_sending_verified_at')->get()
|
||||
->each(function ($domain) {
|
||||
try {
|
||||
$result = $domain->checkVerificationForSending();
|
||||
|
||||
35
app/Console/Commands/ClearOutboundMessages.php
Normal file
35
app/Console/Commands/ClearOutboundMessages.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\OutboundMessage;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearOutboundMessages extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'anonaddy:clear-outbound-messages';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clears outbound messages that are older than 7 days';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
OutboundMessage::where('created_at', '<', now()->subDays(7))->delete();
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\PostfixQueueId;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearPostfixQueueIds extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'anonaddy:clear-postfix-queue-ids';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clears postfix queue ids that are older than 7 days';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
PostfixQueueId::where('created_at', '<=', now()->subDays(7))->delete();
|
||||
}
|
||||
}
|
||||
63
app/Console/Commands/EmailUsersWithTokenExpiringSoon.php
Normal file
63
app/Console/Commands/EmailUsersWithTokenExpiringSoon.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mail\TokenExpiringSoon;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class EmailUsersWithTokenExpiringSoon extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'anonaddy:email-users-with-token-expiring-soon';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Send an email to users who have an API token that is expiring soon';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
User::with(['defaultUsername', 'defaultRecipient'])
|
||||
->whereHas('tokens', function ($query) {
|
||||
$query->whereDate('expires_at', now()->addWeek());
|
||||
})
|
||||
->get()
|
||||
->each(function (User $user) {
|
||||
$this->sendTokenExpiringSoonMail($user);
|
||||
});
|
||||
}
|
||||
|
||||
protected function sendTokenExpiringSoonMail(User $user)
|
||||
{
|
||||
try {
|
||||
Mail::to($user->email)->send(new TokenExpiringSoon($user));
|
||||
} catch (Exception $exception) {
|
||||
$this->error("exception when sending mail to user: {$user->username}", $exception);
|
||||
report($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,7 @@ use App\Mail\SendFromEmail;
|
||||
use App\Models\Alias;
|
||||
use App\Models\Domain;
|
||||
use App\Models\EmailData;
|
||||
use App\Models\PostfixQueueId;
|
||||
use App\Models\Recipient;
|
||||
use App\Models\OutboundMessage;
|
||||
use App\Models\Username;
|
||||
use App\Notifications\DisallowedReplySendAttempt;
|
||||
use App\Notifications\FailedDeliveryNotification;
|
||||
@ -21,6 +20,7 @@ use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use ParagonIE\ConstantTime\Base32;
|
||||
use PhpMimeMailParser\Parser;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
@ -53,6 +53,8 @@ class ReceiveEmail extends Command
|
||||
|
||||
protected $size;
|
||||
|
||||
protected $rawEmail;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
@ -86,9 +88,44 @@ class ReceiveEmail extends Command
|
||||
$this->size = $this->option('size') / ($recipientCount ? $recipientCount : 1);
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
// Handle bounces
|
||||
if ($this->option('sender') === 'MAILER-DAEMON') {
|
||||
$this->handleBounce($recipient['email']);
|
||||
// Check if VERP bounce
|
||||
if (substr($recipient['email'], 0, 2) === 'b_') {
|
||||
if ($outboundMessageId = $this->getIdFromVerp($recipient['email'])) {
|
||||
// Is a valid bounce
|
||||
$outboundMessage = OutboundMessage::with(['user', 'alias', 'recipient'])->find($outboundMessageId);
|
||||
|
||||
if (is_null($outboundMessage)) {
|
||||
// Must have been more than 7 days
|
||||
Log::info('VERP outboundMessage not found');
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$bouncedAlias = $outboundMessage->alias;
|
||||
|
||||
// If already bounced then forward to the user instead
|
||||
if (! $outboundMessage->bounced) {
|
||||
$this->handleBounce($outboundMessage);
|
||||
}
|
||||
|
||||
if (in_array(strtolower($this->parser->getHeader('Auto-Submitted')), ['auto-replied', 'auto-generated']) && ! in_array($outboundMessage->email_type, ['R', 'S'])) {
|
||||
Log::info('VERP auto-response to forward/notification, username: '.$outboundMessage->user?->username.' outboundMessageID: '.$outboundMessageId);
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// If it is a notification then there is no alias so exit and log, may be an auto-reply to a notification.
|
||||
if (is_null($bouncedAlias)) {
|
||||
Log::info('VERP previously bounced/auto-response to notification, username: '.$outboundMessage->user?->username.' outboundMessageID: '.$outboundMessageId);
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// If it is not a bounce (could be auto-reply) then redirect to alias
|
||||
$recipient['email'] = $bouncedAlias->email;
|
||||
$recipient['local_part'] = $bouncedAlias->local_part;
|
||||
$recipient['domain'] = $bouncedAlias->domain;
|
||||
}
|
||||
}
|
||||
|
||||
// First determine if the alias already exists in the database
|
||||
@ -155,7 +192,7 @@ class ReceiveEmail extends Command
|
||||
|
||||
if ($verifiedRecipient?->can_reply_send) {
|
||||
// Check if the Dmarc allow or spam headers are present from Rspamd
|
||||
if (! $this->parser->getHeader('X-AnonAddy-Dmarc-Allow') || $this->parser->getHeader('X-AnonAddy-Spam')) {
|
||||
if (! $this->parser->getHeader('X-AnonAddy-Dmarc-Allow')) {
|
||||
// Notify user and exit
|
||||
$verifiedRecipient->notify(new SpamReplySendAttempt($recipient, $this->senderFrom, $this->parser->getHeader('X-AnonAddy-Authentication-Results')));
|
||||
|
||||
@ -173,7 +210,8 @@ class ReceiveEmail extends Command
|
||||
|
||||
exit(0);
|
||||
} else {
|
||||
$this->handleForward($user, $recipient, $alias ?? null, $aliasable ?? null);
|
||||
// Check if the spam header is present from Rspamd
|
||||
$this->handleForward($user, $recipient, $alias ?? null, $aliasable ?? null, $this->parser->getHeader('X-AnonAddy-Spam') === 'Yes');
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
@ -229,7 +267,7 @@ class ReceiveEmail extends Command
|
||||
Mail::to($sendTo)->queue($message);
|
||||
}
|
||||
|
||||
protected function handleForward($user, $recipient, $alias, $aliasable)
|
||||
protected function handleForward($user, $recipient, $alias, $aliasable, $isSpam)
|
||||
{
|
||||
if (is_null($alias)) {
|
||||
// This is a new alias
|
||||
@ -277,14 +315,14 @@ class ReceiveEmail extends Command
|
||||
|
||||
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size);
|
||||
|
||||
$alias->verifiedRecipientsOrDefault()->each(function ($recipient) use ($alias, $emailData) {
|
||||
$message = new ForwardEmail($alias, $emailData, $recipient);
|
||||
$alias->verifiedRecipientsOrDefault()->each(function ($recipient) use ($alias, $emailData, $isSpam) {
|
||||
$message = (new ForwardEmail($alias, $emailData, $recipient, $isSpam));
|
||||
|
||||
Mail::to($recipient->email)->queue($message);
|
||||
});
|
||||
}
|
||||
|
||||
protected function handleBounce($returnPath)
|
||||
protected function handleBounce($outboundMessage)
|
||||
{
|
||||
// Collect the attachments
|
||||
$attachments = collect($this->parser->getAttachments());
|
||||
@ -294,136 +332,103 @@ class ReceiveEmail extends Command
|
||||
return $attachment->getContentType() === 'message/delivery-status';
|
||||
})->first();
|
||||
|
||||
if ($deliveryReport) {
|
||||
$dsn = $this->parseDeliveryStatus($deliveryReport->getMimePartStr());
|
||||
// Is not a bounce, may be an auto-reply so return
|
||||
if (! $deliveryReport) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify queue ID
|
||||
if (isset($dsn['X-postfix-queue-id'])) {
|
||||
// First check in DB
|
||||
$postfixQueueId = PostfixQueueId::firstWhere('queue_id', strtoupper($dsn['X-postfix-queue-id']));
|
||||
// Mark the outboundMessage as bounced
|
||||
$outboundMessage->markAsBounced();
|
||||
|
||||
if (! $postfixQueueId) {
|
||||
exit(0);
|
||||
}
|
||||
$dsn = $this->parseDeliveryStatus($deliveryReport->getMimePartStr());
|
||||
|
||||
// If found then delete from DB
|
||||
$postfixQueueId->delete();
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
// Get the bounced email address
|
||||
$bouncedEmailAddress = isset($dsn['Final-recipient']) ? trim(Str::after($dsn['Final-recipient'], ';')) : null;
|
||||
|
||||
// Get the bounced email address
|
||||
$bouncedEmailAddress = isset($dsn['Final-recipient']) ? trim(Str::after($dsn['Final-recipient'], ';')) : '';
|
||||
$remoteMta = isset($dsn['Remote-mta']) ? trim(Str::after($dsn['Remote-mta'], ';')) : '';
|
||||
|
||||
$remoteMta = isset($dsn['Remote-mta']) ? trim(Str::after($dsn['Remote-mta'], ';')) : '';
|
||||
if (isset($dsn['Diagnostic-code']) && isset($dsn['Status'])) {
|
||||
// Try to determine the bounce type, HARD, SPAM, SOFT
|
||||
$bounceType = $this->getBounceType($dsn['Diagnostic-code'], $dsn['Status']);
|
||||
|
||||
if (isset($dsn['Diagnostic-code']) && isset($dsn['Status'])) {
|
||||
// Try to determine the bounce type, HARD, SPAM, SOFT
|
||||
$bounceType = $this->getBounceType($dsn['Diagnostic-code'], $dsn['Status']);
|
||||
$diagnosticCode = Str::limit($dsn['Diagnostic-code'], 497);
|
||||
} else {
|
||||
$bounceType = null;
|
||||
$diagnosticCode = null;
|
||||
}
|
||||
|
||||
$diagnosticCode = Str::limit($dsn['Diagnostic-code'], 497);
|
||||
} else {
|
||||
$bounceType = null;
|
||||
$diagnosticCode = null;
|
||||
}
|
||||
// Get the undelivered message
|
||||
$undeliveredMessage = $attachments->filter(function ($attachment) {
|
||||
return in_array($attachment->getContentType(), ['text/rfc822-headers', 'message/rfc822']);
|
||||
})->first();
|
||||
|
||||
// The return path is the alias except when it is from an unverified custom domain
|
||||
if ($returnPath !== config('anonaddy.return_path')) {
|
||||
$alias = Alias::withTrashed()->firstWhere('email', $returnPath);
|
||||
$undeliveredMessageHeaders = [];
|
||||
|
||||
if (isset($alias)) {
|
||||
$user = $alias->user;
|
||||
}
|
||||
}
|
||||
if ($undeliveredMessage) {
|
||||
$undeliveredMessageHeaders = $this->parseDeliveryStatus($undeliveredMessage->getMimePartStr());
|
||||
}
|
||||
|
||||
// Try to find a user from the bounced email address
|
||||
if ($recipient = Recipient::select(['id', 'user_id', 'email', 'email_verified_at'])->get()->firstWhere('email', $bouncedEmailAddress)) {
|
||||
if (! isset($user)) {
|
||||
$user = $recipient->user;
|
||||
}
|
||||
}
|
||||
// Get bounce user information
|
||||
$user = $outboundMessage->user;
|
||||
$alias = $outboundMessage->alias;
|
||||
$recipient = $outboundMessage->recipient;
|
||||
$emailType = $outboundMessage->getRawOriginal('email_type');
|
||||
|
||||
// Get the undelivered message
|
||||
$undeliveredMessage = $attachments->filter(function ($attachment) {
|
||||
return in_array($attachment->getContentType(), ['text/rfc822-headers', 'message/rfc822']);
|
||||
})->first();
|
||||
|
||||
$undeliveredMessageHeaders = [];
|
||||
$emailType = null;
|
||||
if ($user) {
|
||||
$failedDeliveryId = Uuid::uuid4();
|
||||
|
||||
if ($undeliveredMessage) {
|
||||
$undeliveredMessageHeaders = $this->parseDeliveryStatus($undeliveredMessage->getMimePartStr());
|
||||
|
||||
if (isset($undeliveredMessageHeaders['Feedback-id'])) {
|
||||
[$emailType, $aliasId] = explode(':', $undeliveredMessageHeaders['Feedback-id']);
|
||||
|
||||
if (in_array($emailType, ['F', 'R', 'S']) && ! isset($alias)) {
|
||||
$alias = Alias::find($aliasId);
|
||||
|
||||
// Find the user from the alias if we don't have it from the recipient
|
||||
if (! isset($user) && isset($alias)) {
|
||||
$user = $alias->user;
|
||||
}
|
||||
}
|
||||
// Store the undelivered message if enabled by user. Do not store email verification notifications.
|
||||
if ($user->store_failed_deliveries && ! in_array($emailType, ['VR', 'VU'])) {
|
||||
$isStored = Storage::disk('local')->put("{$failedDeliveryId}.eml", $this->trimUndeliveredMessage($undeliveredMessage->getMimePartStr()));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($user)) {
|
||||
$failedDeliveryId = Uuid::uuid4();
|
||||
$failedDelivery = $user->failedDeliveries()->create([
|
||||
'id' => $failedDeliveryId,
|
||||
'recipient_id' => $recipient->id ?? null,
|
||||
'alias_id' => $alias->id ?? null,
|
||||
'is_stored' => $isStored ?? false,
|
||||
'bounce_type' => $bounceType,
|
||||
'remote_mta' => $remoteMta ?? null,
|
||||
'sender' => $undeliveredMessageHeaders['X-anonaddy-original-sender'] ?? null,
|
||||
'destination' => $bouncedEmailAddress,
|
||||
'email_type' => $emailType,
|
||||
'status' => $dsn['Status'] ?? null,
|
||||
'code' => $diagnosticCode,
|
||||
'attempted_at' => $outboundMessage->created_at,
|
||||
]);
|
||||
|
||||
if ($undeliveredMessage) {
|
||||
|
||||
// Store the undelivered message if enabled by user.
|
||||
if ($user->store_failed_deliveries) {
|
||||
$isStored = Storage::disk('local')->put("{$failedDeliveryId}.eml", $this->trimUndeliveredMessage($undeliveredMessage->getMimePartStr()));
|
||||
}
|
||||
// Check the aliases failed deliveries
|
||||
if ($alias) {
|
||||
// Decrement the alias forward count due to failed delivery
|
||||
if ($failedDelivery->getRawOriginal('email_type') === 'F' && $alias->emails_forwarded > 0) {
|
||||
$alias->decrement('emails_forwarded');
|
||||
}
|
||||
|
||||
$failedDelivery = $user->failedDeliveries()->create([
|
||||
'id' => $failedDeliveryId,
|
||||
'recipient_id' => $recipient->id ?? null,
|
||||
'alias_id' => $alias->id ?? null,
|
||||
'is_stored' => $isStored ?? false,
|
||||
'bounce_type' => $bounceType,
|
||||
'remote_mta' => $remoteMta ?? null,
|
||||
'sender' => $undeliveredMessageHeaders['X-anonaddy-original-sender'] ?? null,
|
||||
'email_type' => $emailType ?? null,
|
||||
'status' => $dsn['Status'] ?? null,
|
||||
'code' => $diagnosticCode,
|
||||
'attempted_at' => $postfixQueueId->created_at,
|
||||
]);
|
||||
|
||||
if (isset($alias)) {
|
||||
// Decrement the alias forward count due to failed delivery
|
||||
if ($failedDelivery->email_type === 'F' && $alias->emails_forwarded > 0) {
|
||||
$alias->decrement('emails_forwarded');
|
||||
}
|
||||
|
||||
if ($failedDelivery->email_type === 'R' && $alias->emails_replied > 0) {
|
||||
$alias->decrement('emails_replied');
|
||||
}
|
||||
|
||||
if ($failedDelivery->email_type === 'S' && $alias->emails_sent > 0) {
|
||||
$alias->decrement('emails_sent');
|
||||
}
|
||||
if ($failedDelivery->getRawOriginal('email_type') === 'R' && $alias->emails_replied > 0) {
|
||||
$alias->decrement('emails_replied');
|
||||
}
|
||||
|
||||
if ($failedDelivery->getRawOriginal('email_type') === 'S' && $alias->emails_sent > 0) {
|
||||
$alias->decrement('emails_sent');
|
||||
}
|
||||
} else {
|
||||
Log::info([
|
||||
'info' => 'user not found from bounce report',
|
||||
'deliveryReport' => $deliveryReport,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
Log::info('User not found from outbound message, may have been deleted.');
|
||||
}
|
||||
|
||||
// Check if the bounce is a Failed delivery notification and if so do not notify the user again
|
||||
if (! in_array($emailType, ['FDN'])) {
|
||||
// Check if the bounce is a Failed delivery notification and if so do not notify the user again
|
||||
if (! in_array($emailType, ['FDN'])) {
|
||||
|
||||
$notifiable = $recipient?->email_verified_at ? $recipient : $user?->defaultRecipient;
|
||||
$notifiable = $recipient?->email_verified_at ? $recipient : $user?->defaultRecipient;
|
||||
|
||||
// Notify user of failed delivery
|
||||
if ($notifiable?->email_verified_at) {
|
||||
// Notify user of failed delivery
|
||||
if ($notifiable?->email_verified_at) {
|
||||
|
||||
$notifiable->notify(new FailedDeliveryNotification($alias->email ?? null, $undeliveredMessageHeaders['X-anonaddy-original-sender'] ?? null, $undeliveredMessageHeaders['Subject'] ?? null, $failedDelivery?->is_stored, $user?->store_failed_deliveries, $recipient?->email));
|
||||
}
|
||||
$notifiable->notify(new FailedDeliveryNotification($alias->email ?? null, $undeliveredMessageHeaders['X-anonaddy-original-sender'] ?? null, $undeliveredMessageHeaders['Subject'] ?? null, $failedDelivery?->is_stored, $user?->store_failed_deliveries, $recipient?->email));
|
||||
|
||||
Log::info('FDN '.$emailType.': '.$notifiable->email);
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,6 +438,8 @@ class ReceiveEmail extends Command
|
||||
protected function checkBandwidthLimit($user)
|
||||
{
|
||||
if ($user->hasReachedBandwidthLimit()) {
|
||||
$user->update(['reject_until' => now()->endOfMonth()]);
|
||||
|
||||
$this->error('4.2.1 Bandwidth limit exceeded for user. Please try again later.');
|
||||
|
||||
exit(1);
|
||||
@ -453,7 +460,9 @@ class ReceiveEmail extends Command
|
||||
->then(
|
||||
function () {
|
||||
},
|
||||
function () {
|
||||
function () use ($user) {
|
||||
$user->update(['defer_until' => now()->addHour()]);
|
||||
|
||||
$this->error('4.2.1 Rate limit exceeded for user. Please try again later.');
|
||||
|
||||
exit(1);
|
||||
@ -532,6 +541,11 @@ class ReceiveEmail extends Command
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function trimUndeliveredMessage($message)
|
||||
{
|
||||
return Str::after($message, 'Content-Type: message/rfc822'.PHP_EOL.PHP_EOL);
|
||||
}
|
||||
|
||||
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)) {
|
||||
@ -553,12 +567,48 @@ class ReceiveEmail extends Command
|
||||
protected function getSenderFrom()
|
||||
{
|
||||
try {
|
||||
return $this->parser->getAddresses('from')[0]['address'];
|
||||
// Ensure contains '@', may be malformed header which causes sends/replies to fail
|
||||
$address = $this->parser->getAddresses('from')[0]['address'];
|
||||
|
||||
return Str::contains($address, '@') ? $address : $this->option('sender');
|
||||
} catch (\Exception $e) {
|
||||
return $this->option('sender');
|
||||
}
|
||||
}
|
||||
|
||||
protected function getIdFromVerp($verp)
|
||||
{
|
||||
$localPart = Str::beforeLast($verp, '@');
|
||||
|
||||
$parts = explode('_', $localPart);
|
||||
|
||||
if (count($parts) !== 3) {
|
||||
Log::channel('single')->info('VERP invalid email: '.$verp);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$id = Base32::decodeNoPadding($parts[1]);
|
||||
|
||||
$signature = Base32::decodeNoPadding($parts[2]);
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('single')->info('VERP base32 decode failure: '.$verp.' '.$e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$expectedSignature = substr(hash_hmac('sha3-224', $id, config('anonaddy.secret')), 0, 8);
|
||||
|
||||
if ($signature !== $expectedSignature) {
|
||||
Log::channel('single')->info('VERP invalid signature: '.$verp);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
protected function exitIfFromSelf()
|
||||
{
|
||||
// To prevent recipient alias infinite nested looping.
|
||||
|
||||
@ -38,6 +38,6 @@ class ResetBandwidth extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
User::where('bandwidth', '>', 0)->update(['bandwidth' => 0]);
|
||||
User::where('bandwidth', '>', 0)->update(['bandwidth' => 0, 'reject_until' => null, 'defer_until' => null]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,8 +27,10 @@ class Kernel extends ConsoleKernel
|
||||
$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-postfix-queue-ids')->hourly();
|
||||
$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();
|
||||
}
|
||||
|
||||
|
||||
@ -4,14 +4,14 @@ namespace App\CustomMailDriver;
|
||||
|
||||
use App\CustomMailDriver\Mime\Crypto\AlreadyEncrypted;
|
||||
use App\CustomMailDriver\Mime\Crypto\OpenPGPEncrypter;
|
||||
use App\Models\PostfixQueueId;
|
||||
use App\Models\OutboundMessage;
|
||||
use App\Models\Recipient;
|
||||
use App\Notifications\GpgKeyExpired;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Mail\Mailable as MailableContract;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Mail\Mailer;
|
||||
use Illuminate\Mail\SentMessage;
|
||||
use Illuminate\Support\Str;
|
||||
use ParagonIE\ConstantTime\Base32;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Exception\RuntimeException;
|
||||
use Symfony\Component\Mime\Crypto\DkimOptions;
|
||||
@ -121,6 +121,17 @@ class CustomMailer extends Mailer
|
||||
}
|
||||
|
||||
if ($this->shouldSendMessage($symfonyMessage, $data)) {
|
||||
// Set VERP address
|
||||
$id = randomString(12);
|
||||
$verpLocalPart = $this->getVerpLocalPart($id);
|
||||
|
||||
// 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'])) {
|
||||
$message->returnPath($verpLocalPart.'@'.$data['verpDomain']);
|
||||
} else {
|
||||
$message->returnPath($verpLocalPart.'@'.config('anonaddy.domain'));
|
||||
}
|
||||
|
||||
$symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage);
|
||||
|
||||
if ($symfonySentMessage) {
|
||||
@ -128,16 +139,20 @@ class CustomMailer extends Mailer
|
||||
|
||||
$this->dispatchSentEvent($sentMessage, $data);
|
||||
|
||||
try {
|
||||
// Get Postfix Queue ID and save in DB
|
||||
$id = str_replace("\r\n", '', Str::after($sentMessage->getDebug(), 'Ok: queued as '));
|
||||
// Create a new Outbound Message for verifying any bounces
|
||||
if (isset($data['userId']) && ! is_null($data['userId']) && isset($data['emailType']) && ! is_null($data['emailType'])) {
|
||||
|
||||
PostfixQueueId::create([
|
||||
'queue_id' => $id,
|
||||
]);
|
||||
} catch (QueryException $e) {
|
||||
// duplicate entry
|
||||
//Log::info('Failed to save Postfix Queue ID: ' . $id);
|
||||
try {
|
||||
OutboundMessage::create([
|
||||
'id' => $id,
|
||||
'user_id' => $data['userId'],
|
||||
'alias_id' => $data['aliasId'] ?? null,
|
||||
'recipient_id' => $data['recipientId'] ?? null,
|
||||
'email_type' => $data['emailType'],
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
return $sentMessage;
|
||||
@ -171,4 +186,14 @@ class CustomMailer extends Mailer
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
protected function getVerpLocalPart($id)
|
||||
{
|
||||
$hmac = hash_hmac('sha3-224', $id, config('anonaddy.secret'));
|
||||
$hmacPayload = substr($hmac, 0, 8);
|
||||
$encodedPayload = Base32::encodeUnpadded($id);
|
||||
$encodedSignature = Base32::encodeUnpadded($hmacPayload);
|
||||
|
||||
return "b_{$encodedPayload}_{$encodedSignature}";
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ class EncryptedPart extends AbstractPart
|
||||
$this->body = $body;
|
||||
$this->charset = $charset;
|
||||
$this->subtype = $subtype;
|
||||
$this->seekable = \is_resource($body) ? stream_get_meta_data($body)['seekable'] && 0 === fseek($body, 0, \SEEK_CUR) : null;
|
||||
$this->seekable = \is_resource($body) ? stream_get_meta_data($body)['seekable'] && fseek($body, 0, \SEEK_CUR) === 0 : null;
|
||||
}
|
||||
|
||||
public function getMediaType(): string
|
||||
@ -56,7 +56,7 @@ class EncryptedPart extends AbstractPart
|
||||
|
||||
public function getBody(): string
|
||||
{
|
||||
if (null === $this->seekable) {
|
||||
if ($this->seekable === null) {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ class EncryptedPart extends AbstractPart
|
||||
|
||||
public function bodyToIterable(): iterable
|
||||
{
|
||||
if (null !== $this->seekable) {
|
||||
if ($this->seekable !== null) {
|
||||
if ($this->seekable) {
|
||||
rewind($this->body);
|
||||
}
|
||||
@ -92,10 +92,10 @@ class EncryptedPart extends AbstractPart
|
||||
public function asDebugString(): string
|
||||
{
|
||||
$str = parent::asDebugString();
|
||||
if (null !== $this->charset) {
|
||||
if ($this->charset !== null) {
|
||||
$str .= ' charset: '.$this->charset;
|
||||
}
|
||||
if (null !== $this->disposition) {
|
||||
if ($this->disposition !== null) {
|
||||
$str .= ' disposition: '.$this->disposition;
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ class EncryptedPart extends AbstractPart
|
||||
public function __sleep(): array
|
||||
{
|
||||
// convert resources to strings for serialization
|
||||
if (null !== $this->seekable) {
|
||||
if ($this->seekable !== null) {
|
||||
$this->body = $this->getBody();
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,21 @@ class InlineImagePart extends DataPart
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContentId(): string
|
||||
{
|
||||
return $this->cid ?: $this->cid = $this->generateContentId();
|
||||
}
|
||||
|
||||
public function hasContentId(): bool
|
||||
{
|
||||
return $this->cid !== null;
|
||||
}
|
||||
|
||||
private function generateContentId(): string
|
||||
{
|
||||
return bin2hex(random_bytes(16)).'@symfony';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the file.
|
||||
*
|
||||
@ -35,11 +50,11 @@ class InlineImagePart extends DataPart
|
||||
{
|
||||
$headers = parent::getPreparedHeaders();
|
||||
|
||||
if (null !== $this->cid) {
|
||||
if ($this->cid !== null) {
|
||||
$headers->setHeaderBody('Id', 'Content-ID', $this->cid);
|
||||
}
|
||||
|
||||
if (null !== $this->filename) {
|
||||
if ($this->filename !== null) {
|
||||
$headers->setHeaderParameter('Content-Disposition', 'filename', $this->filename);
|
||||
}
|
||||
|
||||
|
||||
14
app/Enums/DisplayFromFormat.php
Normal file
14
app/Enums/DisplayFromFormat.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum DisplayFromFormat: int
|
||||
{
|
||||
case DEFAULT = 0;
|
||||
case BRACKETS = 1;
|
||||
case DOMAIN = 2;
|
||||
case NAME = 3;
|
||||
case ADDRESS = 4;
|
||||
case NONE = 5;
|
||||
case DOMAINONLY = 6;
|
||||
}
|
||||
@ -11,3 +11,17 @@ function carbon(...$args)
|
||||
{
|
||||
return new Carbon(...$args);
|
||||
}
|
||||
|
||||
function randomString(int $length): string
|
||||
{
|
||||
$alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
$str = '';
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$index = random_int(0, 35);
|
||||
$str .= $alphabet[$index];
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
@ -9,6 +9,10 @@ class AliasExportController extends Controller
|
||||
{
|
||||
public function export()
|
||||
{
|
||||
if (! user()->allAliases()->count()) {
|
||||
return back()->withErrors(['aliases_export' => 'You don\'t have any aliases to export.']);
|
||||
}
|
||||
|
||||
return Excel::download(new AliasesExport(), 'aliases-'.now()->toDateString().'.csv');
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,6 @@ class AliasImportController extends Controller
|
||||
report($e);
|
||||
}
|
||||
|
||||
return back()->with(['status' => 'File uploaded successfully, your aliases are being imported']);
|
||||
return back()->with(['flash' => 'File uploaded successfully, your aliases are being imported']);
|
||||
}
|
||||
}
|
||||
|
||||
252
app/Http/Controllers/Api/AliasBulkController.php
Normal file
252
app/Http/Controllers/Api/AliasBulkController.php
Normal file
@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\AliasResource;
|
||||
use App\Rules\VerifiedRecipientId;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class AliasBulkController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('throttle:12,1');
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// If there are no aliases found return 404 response
|
||||
if (! $aliases->count()) {
|
||||
return response()->json(['message' => 'No aliases found'], 404);
|
||||
}
|
||||
|
||||
return AliasResource::collection($aliases);
|
||||
}
|
||||
|
||||
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)
|
||||
->whereIn('id', $request->ids)
|
||||
->get();
|
||||
|
||||
// If there are no aliases found return 404 response
|
||||
if (! $aliasesWithTrashedCount = $aliasesWithTrashed->count()) {
|
||||
return response()->json(['message' => 'No aliases found'], 404);
|
||||
}
|
||||
|
||||
// Check if all aliases are deleted, if so return message
|
||||
$aliases = $aliasesWithTrashed->filter(function ($alias) {
|
||||
return ! $alias->trashed();
|
||||
});
|
||||
|
||||
if ($aliases->count() === 0) {
|
||||
return response()->json([
|
||||
'message' => $aliasesWithTrashedCount === 1 ? 'You need to restore this alias before you can activate it' : 'You need to restore these aliases before you can activate them',
|
||||
'ids' => $aliasesWithTrashed->pluck('id'),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$aliasIds = $aliases->pluck('id')->all();
|
||||
$aliasIdsCount = count($aliasIds);
|
||||
user()->aliases()->whereIn('id', $aliasIds)->update(['active' => true]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $aliasIdsCount === 1 ? '1 alias activated successfully' : "{$aliasIdsCount} aliases activated successfully",
|
||||
'ids' => $aliasIds,
|
||||
], 200);
|
||||
}
|
||||
|
||||
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)
|
||||
->pluck('id');
|
||||
|
||||
// If there are no aliases found return 404 response
|
||||
if (! $aliasIdsCount = $aliasIds->count()) {
|
||||
return response()->json(['message' => 'No aliases found'], 404);
|
||||
}
|
||||
|
||||
user()->aliases()->whereIn('id', $aliasIds)->update(['active' => false]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $aliasIdsCount === 1 ? '1 alias deactivated successfully' : "{$aliasIdsCount} aliases deactivated successfully",
|
||||
'ids' => $aliasIds,
|
||||
], 200);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// If there are no aliases found return 404 response
|
||||
if (! $aliasIdsCount = $aliasIds->count()) {
|
||||
return response()->json(['message' => 'No aliases found'], 404);
|
||||
}
|
||||
|
||||
// Detach any recipients
|
||||
DB::table('alias_recipients')->whereIn('alias_id', $aliasIds)->delete();
|
||||
|
||||
// Use update since delete() does not trigger model event
|
||||
user()->aliases()->whereIn('id', $aliasIds)->update(['active' => false, 'deleted_at' => now()]);
|
||||
|
||||
// Don't return 204 as that is only for empty responses
|
||||
return response()->json([
|
||||
'message' => $aliasIdsCount === 1 ? '1 alias deleted successfully' : "{$aliasIdsCount} aliases deleted successfully",
|
||||
'ids' => $aliasIds,
|
||||
], 200);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// If there are no aliases found return 404 response
|
||||
if (! $aliasIdsCount = $aliasIds->count()) {
|
||||
return response()->json(['message' => 'No aliases found'], 404);
|
||||
}
|
||||
|
||||
// Detach any recipients
|
||||
DB::table('alias_recipients')->whereIn('alias_id', $aliasIds)->delete();
|
||||
|
||||
// Shared Domain aliases, remove all data and change user_id
|
||||
$forgottenSharedDomainCount = user()->aliases()->withTrashed()
|
||||
->whereIn('id', $aliasIds)
|
||||
->whereIn('domain', config('anonaddy.all_domains'))
|
||||
->update([
|
||||
'user_id' => '00000000-0000-0000-0000-000000000000',
|
||||
'extension' => null,
|
||||
'description' => null,
|
||||
'emails_forwarded' => 0,
|
||||
'emails_blocked' => 0,
|
||||
'emails_replied' => 0,
|
||||
'emails_sent' => 0,
|
||||
'active' => false,
|
||||
'deleted_at' => now(),
|
||||
]);
|
||||
|
||||
if ($forgottenSharedDomainCount < $aliasIdsCount) {
|
||||
// Standard aliases
|
||||
user()->aliases()->withTrashed()
|
||||
->whereIn('id', $aliasIds)
|
||||
->whereNotIn('domain', config('anonaddy.all_domains'))
|
||||
->forceDelete();
|
||||
}
|
||||
|
||||
// Don't return 204 as that is only for empty responses
|
||||
return response()->json([
|
||||
'message' => $aliasIdsCount === 1 ? '1 alias forgotten successfully' : "{$aliasIdsCount} aliases forgotten successfully",
|
||||
'ids' => $aliasIds,
|
||||
], 200);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// If there are no aliases found return 404 response
|
||||
if (! $aliasIdsCount = $aliasIds->count()) {
|
||||
return response()->json(['message' => 'No aliases found'], 404);
|
||||
}
|
||||
|
||||
// Use update since delete() does not trigger model event
|
||||
user()->aliases()->onlyTrashed()->whereIn('id', $aliasIds)->update(['active' => true, 'deleted_at' => null]);
|
||||
|
||||
// Don't return 204 as that is only for empty responses
|
||||
return response()->json([
|
||||
'message' => $aliasIdsCount === 1 ? '1 alias restored successfully' : "{$aliasIdsCount} aliases restored successfully",
|
||||
'ids' => $aliasIds,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function recipients(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|max:25|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
'recipient_ids' => [
|
||||
'array',
|
||||
'max:10',
|
||||
new VerifiedRecipientId(),
|
||||
],
|
||||
'recipient_ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
||||
$aliasIds = user()->aliases()->withTrashed()
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
||||
// If there are no aliases found return 404 response
|
||||
if (! $aliasIdsCount = $aliasIds->count()) {
|
||||
return response()->json(['message' => 'No aliases found'], 404);
|
||||
}
|
||||
|
||||
// First delete existing alias recipients
|
||||
DB::table('alias_recipients')->whereIn('alias_id', $aliasIds)->delete();
|
||||
// Then create alias recipients
|
||||
DB::table('alias_recipients')->insert((collect($aliasIds))->flatMap(function ($aliasId) use ($request) {
|
||||
$val = [];
|
||||
foreach ($request->recipient_ids as $recipientId) {
|
||||
$val[] = [
|
||||
'id' => Uuid::uuid4(),
|
||||
'alias_id' => $aliasId,
|
||||
'recipient_id' => $recipientId,
|
||||
];
|
||||
}
|
||||
|
||||
return $val;
|
||||
})->all());
|
||||
|
||||
// Don't return 204 as that is only for empty responses
|
||||
return response()->json([
|
||||
'message' => $aliasIdsCount === 1 ? 'recipients updated for 1 alias successfully' : "recipients updated for {$aliasIdsCount} aliases successfully",
|
||||
'ids' => $aliasIds,
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
@ -73,8 +73,8 @@ class AliasController extends Controller
|
||||
return response('You have reached your hourly limit for creating new aliases', 429);
|
||||
}
|
||||
|
||||
if (isset($request->validated()['local_part'])) {
|
||||
$localPart = $request->validated()['local_part'];
|
||||
if (isset($request->validated()['local_part_without_extension'])) {
|
||||
$localPart = $request->local_part; // To get the local_part with any potential extension
|
||||
|
||||
// Local part has extension
|
||||
if (Str::contains($localPart, '+')) {
|
||||
@ -153,7 +153,15 @@ class AliasController extends Controller
|
||||
{
|
||||
$alias = user()->aliases()->withTrashed()->findOrFail($id);
|
||||
|
||||
$alias->update(['description' => $request->description]);
|
||||
if ($request->has('description')) {
|
||||
$alias->description = $request->description;
|
||||
}
|
||||
|
||||
if ($request->has('from_name')) {
|
||||
$alias->from_name = $request->from_name;
|
||||
}
|
||||
|
||||
$alias->save();
|
||||
|
||||
return new AliasResource($alias->refresh()->load('recipients'));
|
||||
}
|
||||
|
||||
62
app/Http/Controllers/Api/ChartDataController.php
Normal file
62
app/Http/Controllers/Api/ChartDataController.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class ChartDataController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$outboundMessages = user()->outboundMessages()
|
||||
->select(['user_id', 'email_type', 'created_at'])
|
||||
->where('created_at', '>=', now()->subDays(6)->startOfDay())
|
||||
->get()
|
||||
->groupBy(function ($outboundMessage) {
|
||||
return $outboundMessage->created_at->format('l');
|
||||
})
|
||||
->map(function ($group) {
|
||||
return [
|
||||
'forwards' => $group->where('email_type', 'F')->count(),
|
||||
'replies' => $group->where('email_type', 'R')->count(),
|
||||
'sends' => $group->where('email_type', 'S')->count(),
|
||||
];
|
||||
});
|
||||
|
||||
$days = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
|
||||
$today = date('w'); // 0 Sunday
|
||||
|
||||
// Get the days until today including today
|
||||
$previous = array_slice($days, 0, $today + 1);
|
||||
|
||||
// Get remaining days in week
|
||||
$coming = array_slice($days, $today + 1);
|
||||
|
||||
$data = collect(array_merge($coming, $previous))->mapWithKeys(function ($day) use ($outboundMessages) {
|
||||
return [$day => $outboundMessages->get($day, ['forwards' => 0, 'replies' => 0, 'sends' => 0])];
|
||||
});
|
||||
|
||||
$outboundMessageTotals = [
|
||||
$outboundMessages->sum('forwards'),
|
||||
$outboundMessages->sum('replies'),
|
||||
$outboundMessages->sum('sends'),
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'forwardsData' => $data->pluck('forwards'),
|
||||
'repliesData' => $data->pluck('replies'),
|
||||
'sendsData' => $data->pluck('sends'),
|
||||
'labels' => $data->keys(),
|
||||
'outboundMessageTotals' => $outboundMessageTotals,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -47,7 +47,15 @@ class DomainController extends Controller
|
||||
{
|
||||
$domain = user()->domains()->findOrFail($id);
|
||||
|
||||
$domain->update(['description' => $request->description]);
|
||||
if ($request->has('description')) {
|
||||
$domain->description = $request->description;
|
||||
}
|
||||
|
||||
if ($request->has('from_name')) {
|
||||
$domain->from_name = $request->from_name;
|
||||
}
|
||||
|
||||
$domain->save();
|
||||
|
||||
return new DomainResource($domain->refresh()->load(['aliases', 'defaultRecipient']));
|
||||
}
|
||||
|
||||
34
app/Http/Controllers/Api/LoginableUsernameController.php
Normal file
34
app/Http/Controllers/Api/LoginableUsernameController.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\UsernameResource;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LoginableUsernameController extends Controller
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate(['id' => 'required|string']);
|
||||
|
||||
$username = user()->usernames()->findOrFail($request->id);
|
||||
|
||||
$username->allowLogin();
|
||||
|
||||
return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$username = user()->usernames()->findOrFail($id);
|
||||
|
||||
if ($id === user()->default_username_id) {
|
||||
return response('You cannot disallow login for your default username', 403);
|
||||
}
|
||||
|
||||
$username->disallowLogin();
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,15 @@ class UsernameController extends Controller
|
||||
{
|
||||
$username = user()->usernames()->findOrFail($id);
|
||||
|
||||
$username->update(['description' => $request->description]);
|
||||
if ($request->has('description')) {
|
||||
$username->description = $request->description;
|
||||
}
|
||||
|
||||
if ($request->has('from_name')) {
|
||||
$username->from_name = $request->from_name;
|
||||
}
|
||||
|
||||
$username->save();
|
||||
|
||||
return new UsernameResource($username->refresh()->load(['aliases', 'defaultRecipient']));
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ class ApiAuthenticationController extends Controller
|
||||
} elseif (Webauthn::enabled($user)) {
|
||||
// If WebAuthn is enabled then return currently unsupported message
|
||||
return response()->json([
|
||||
'error' => 'WebAuthn 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);
|
||||
}
|
||||
|
||||
|
||||
@ -57,12 +57,19 @@ class BackupCodeController extends Controller
|
||||
return redirect()->intended($request->redirectPath);
|
||||
}
|
||||
|
||||
public function update()
|
||||
public function update(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'current' => 'required|string|current_password',
|
||||
]);
|
||||
|
||||
user()->update([
|
||||
'two_factor_backup_code' => bcrypt($code = Str::random(40)),
|
||||
]);
|
||||
|
||||
return back()->with(['backupCode' => $code]);
|
||||
return back()->with([
|
||||
'flash' => 'New Backup Code Generated Successfully',
|
||||
'regeneratedBackupCode' => $code,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\Username;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
@ -66,7 +66,18 @@ class ForgotPasswordController extends Controller
|
||||
*/
|
||||
protected function validateUsername(Request $request)
|
||||
{
|
||||
$request->validate(['username' => 'required|regex:/^[a-zA-Z0-9]*$/|max:20']);
|
||||
// Validate captcha separately first to prevent username enumeration
|
||||
if (! App::environment('testing')) {
|
||||
$request->validate([
|
||||
'captcha' => 'required|captcha',
|
||||
], [
|
||||
'captcha.captcha' => 'The text entered was incorrect, please try again.',
|
||||
]);
|
||||
}
|
||||
|
||||
$request->validate(['username' => 'required|regex:/^[a-zA-Z0-9]*$/|max:20'], [
|
||||
'username.regex' => 'Your username can only contain letters and numbers, do not use your email.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Recipient;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class ForgotUsernameController extends Controller
|
||||
{
|
||||
@ -54,6 +55,14 @@ class ForgotUsernameController extends Controller
|
||||
*/
|
||||
protected function validateEmail(Request $request)
|
||||
{
|
||||
if (! App::environment('testing')) {
|
||||
$request->validate([
|
||||
'captcha' => 'required|captcha',
|
||||
], [
|
||||
'captcha.captcha' => 'The text entered was incorrect, please try again.',
|
||||
]);
|
||||
}
|
||||
|
||||
$request->validate(['email' => 'required|email:rfc']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,10 @@ 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;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
@ -42,11 +45,17 @@ class LoginController extends Controller
|
||||
|
||||
public function username()
|
||||
{
|
||||
$userId = Username::firstWhere('username', request()->input('username'))?->user_id;
|
||||
return 'id';
|
||||
}
|
||||
|
||||
public function addIdToRequest()
|
||||
{
|
||||
$userId = Username::select(['user_id', 'username', 'can_login'])
|
||||
->where('username', request()->input('username'))
|
||||
->where('can_login', true)
|
||||
->first()?->user_id;
|
||||
|
||||
request()->merge(['id' => $userId]);
|
||||
|
||||
return 'id';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,10 +67,25 @@ class LoginController extends Controller
|
||||
*/
|
||||
protected function validateLogin(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
$this->username() => 'nullable|string',
|
||||
$this->addIdToRequest();
|
||||
|
||||
Validator::make($request->all(), [
|
||||
'username' => 'required|regex:/^[a-zA-Z0-9]*$/|min:1|max:20',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
$this->username() => 'nullable|string',
|
||||
], [
|
||||
'username.regex' => 'Your username can only contain letters and numbers, do not use your email.',
|
||||
])->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the needed authorization credentials from the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function credentials(Request $request)
|
||||
{
|
||||
return $request->only('id', 'password');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,4 +101,28 @@ class LoginController extends Controller
|
||||
'username' => [trans('auth.failed')],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function loggedOut(Request $request)
|
||||
{
|
||||
return Inertia::location(route('login'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StorePersonalAccessTokenRequest;
|
||||
use App\Http\Resources\PersonalAccessTokenResource;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class PersonalAccessTokenController extends Controller
|
||||
{
|
||||
@ -16,6 +18,10 @@ class PersonalAccessTokenController extends Controller
|
||||
|
||||
public function store(StorePersonalAccessTokenRequest $request)
|
||||
{
|
||||
if (! Hash::check($request->password, user()->password)) {
|
||||
throw ValidationException::withMessages(['password' => 'Incorrect password entered']);
|
||||
}
|
||||
|
||||
// day, week, month, year or null
|
||||
if ($request->expiration) {
|
||||
$method = 'add'.ucfirst($request->expiration);
|
||||
|
||||
@ -14,6 +14,7 @@ use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class RegisterController extends Controller
|
||||
@ -55,6 +56,22 @@ class RegisterController extends Controller
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
{
|
||||
// Validate captcha separately first to prevent username enumeration
|
||||
if (! App::environment('testing')) {
|
||||
$validator = Validator::make($data, [
|
||||
'captcha' => [
|
||||
'required',
|
||||
'captcha',
|
||||
],
|
||||
], [
|
||||
'captcha.captcha' => 'The text entered was incorrect, please try again.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $validator;
|
||||
}
|
||||
}
|
||||
|
||||
return Validator::make($data, [
|
||||
'username' => [
|
||||
'required',
|
||||
@ -72,13 +89,10 @@ class RegisterController extends Controller
|
||||
new RegisterUniqueRecipient(),
|
||||
new NotLocalRecipient(),
|
||||
],
|
||||
'password' => ['required', 'min:8'],
|
||||
'password' => ['required', Password::defaults()],
|
||||
], [
|
||||
'captcha.captcha' => 'The text entered was incorrect, please try again.',
|
||||
])
|
||||
->sometimes('captcha', 'required|captcha', function () {
|
||||
return ! App::environment('testing');
|
||||
});
|
||||
'username.regex' => 'Your username can only contain letters and numbers.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Username;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
@ -65,7 +66,11 @@ class ResetPasswordController extends Controller
|
||||
return [
|
||||
'token' => 'required',
|
||||
'username' => 'required|regex:/^[a-zA-Z0-9]*$/|max:20',
|
||||
'password' => 'required|confirmed|min:8',
|
||||
'password' => [
|
||||
'required',
|
||||
'confirmed',
|
||||
Password::defaults(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ namespace App\Http\Controllers\Auth;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\EnableTwoFactorAuthRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use PragmaRX\Google2FALaravel\Support\Authenticator;
|
||||
|
||||
@ -52,14 +51,14 @@ class TwoFactorAuthController extends Controller
|
||||
|
||||
user()->update(['two_factor_secret' => $this->twoFactor->generateSecretKey()]);
|
||||
|
||||
return back()->with(['status' => '2FA Secret Successfully Regenerated']);
|
||||
return back()->with(['flash' => '2FA Secret Successfully Regenerated']);
|
||||
}
|
||||
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
if (! Hash::check($request->current_password_2fa, user()->password)) {
|
||||
return back()->withErrors(['current_password_2fa' => 'Current password incorrect']);
|
||||
}
|
||||
$request->validate([
|
||||
'current' => 'required|string|current_password',
|
||||
]);
|
||||
|
||||
user()->update([
|
||||
'two_factor_enabled' => false,
|
||||
@ -68,7 +67,7 @@ class TwoFactorAuthController extends Controller
|
||||
|
||||
$this->authenticator->logout();
|
||||
|
||||
return back()->with(['status' => '2FA Disabled Successfully']);
|
||||
return back()->with(['flash' => '2FA Disabled Successfully']);
|
||||
}
|
||||
|
||||
public function authenticateTwoFactor(Request $request)
|
||||
|
||||
@ -5,11 +5,14 @@ namespace App\Http\Controllers\Auth;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Recipient;
|
||||
use App\Models\User;
|
||||
use App\Notifications\DefaultRecipientUpdated;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\VerifiesEmails;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class VerificationController extends Controller
|
||||
{
|
||||
@ -46,6 +49,18 @@ class VerificationController extends Controller
|
||||
$this->middleware('throttle:6,1')->only('verify');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the email verification notice.
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect($this->redirectPath())
|
||||
: Inertia::render('Auth/Verify', ['flash' => $request->session()->get('resent', null) ? 'A fresh verification link has been sent to your email address.' : null]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*
|
||||
@ -55,7 +70,7 @@ class VerificationController extends Controller
|
||||
*/
|
||||
public function verify(Request $request)
|
||||
{
|
||||
$verifiable = User::find($request->route('id')) ?? Recipient::find($request->route('id'));
|
||||
$verifiable = User::find($request->route('id')) ?? Recipient::withPending()->find($request->route('id'));
|
||||
|
||||
if (is_null($verifiable)) {
|
||||
throw new AuthorizationException('Email address not found.');
|
||||
@ -83,8 +98,35 @@ class VerificationController extends Controller
|
||||
$redirect = 'login';
|
||||
}
|
||||
|
||||
// Check if the verifiable is a pending new email Recipient
|
||||
if ($verifiable instanceof Recipient && $verifiable->pending) {
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($verifiable) {
|
||||
$user = $verifiable->user;
|
||||
$defaultRecipient = $user->defaultRecipient;
|
||||
// Notify the current default recipient of the change
|
||||
$defaultRecipient->notify(new DefaultRecipientUpdated($verifiable->email));
|
||||
|
||||
// Set verifiable email as new default recipient
|
||||
$defaultRecipient->update([
|
||||
'email' => strtolower($verifiable->email),
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
// Delete pending verifiable
|
||||
$verifiable->delete();
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
|
||||
return redirect($redirect)
|
||||
->with(['flash' => 'An error has occurred, please try again later.']);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect($redirect)
|
||||
->with('verified', true)
|
||||
->with(['status' => 'Email Address Verified Successfully']);
|
||||
->with(['flash' => 'Email Address Verified Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,16 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Facades\Webauthn as WebauthnFacade;
|
||||
use App\Models\WebauthnKey;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Illuminate\Support\Str;
|
||||
use LaravelWebauthn\Actions\PrepareCreationData;
|
||||
use LaravelWebauthn\Actions\ValidateKeyCreation;
|
||||
use LaravelWebauthn\Contracts\DestroyResponse;
|
||||
use LaravelWebauthn\Contracts\RegisterSuccessResponse;
|
||||
use LaravelWebauthn\Contracts\RegisterViewResponse;
|
||||
use LaravelWebauthn\Facades\Webauthn;
|
||||
use LaravelWebauthn\Http\Controllers\WebauthnKeyController as ControllersWebauthnController;
|
||||
use LaravelWebauthn\Http\Requests\WebauthnRegisterRequest;
|
||||
|
||||
@ -24,67 +21,39 @@ class WebauthnController extends ControllersWebauthnController
|
||||
|
||||
/**
|
||||
* Return the register data to attempt a Webauthn registration.
|
||||
*
|
||||
* @return RegisterViewResponse
|
||||
*/
|
||||
public function create(Request $request)
|
||||
/* public function create(Request $request): RegisterViewResponse
|
||||
{
|
||||
$publicKey = app(PrepareCreationData::class)($request->user());
|
||||
|
||||
return app(RegisterViewResponse::class)
|
||||
->setPublicKey($request, $publicKey);
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
* Validate and create the Webauthn request.
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function store(WebauthnRegisterRequest $request)
|
||||
public function store(WebauthnRegisterRequest $request): RegisterSuccessResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'password' => 'required|string|current_password',
|
||||
]);
|
||||
|
||||
try {
|
||||
app(ValidateKeyCreation::class)(
|
||||
$request->user(),
|
||||
$request->only(['id', 'rawId', 'response', 'type']),
|
||||
$request->input('name')
|
||||
);
|
||||
|
||||
user()->update([
|
||||
'two_factor_enabled' => false,
|
||||
]);
|
||||
|
||||
return $this->redirectAfterSuccessRegister();
|
||||
} catch (\Exception $e) {
|
||||
return Response::json([
|
||||
'error' => [
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the redirect destination after a successfull register.
|
||||
*
|
||||
* @param WebauthnKey $webauthnKey
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function redirectAfterSuccessRegister()
|
||||
{
|
||||
// If the user already has at least one key do not generate a new backup code.
|
||||
if (user()->webauthnKeys()->count() > 1) {
|
||||
return Redirect::intended('/settings');
|
||||
}
|
||||
$webauthnKey = app(ValidateKeyCreation::class)(
|
||||
$request->user(),
|
||||
$request->only(['id', 'rawId', 'response', 'type']),
|
||||
$request->input('name')
|
||||
);
|
||||
|
||||
user()->update([
|
||||
'two_factor_backup_code' => bcrypt($code = Str::random(40)),
|
||||
'two_factor_enabled' => false,
|
||||
]);
|
||||
|
||||
return Redirect::intended('/settings')->with(['backupCode' => $code]);
|
||||
return app(RegisterSuccessResponse::class)
|
||||
->setWebauthnKey($request, $webauthnKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,27 +61,27 @@ class WebauthnController extends ControllersWebauthnController
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy(Request $request, $webauthnKeyId)
|
||||
public function destroy(Request $request, $webauthnKeyId): DestroyResponse
|
||||
{
|
||||
try {
|
||||
user()->webauthnKeys()
|
||||
->findOrFail($webauthnKeyId)
|
||||
->delete();
|
||||
$request->validate([
|
||||
'current' => 'required|string|current_password',
|
||||
]);
|
||||
|
||||
if (! WebauthnFacade::hasKey(user())) {
|
||||
WebauthnFacade::logout();
|
||||
}
|
||||
user()->webauthnKeys()
|
||||
->findOrFail($webauthnKeyId)
|
||||
->delete();
|
||||
|
||||
return Response::json([
|
||||
'deleted' => true,
|
||||
'id' => $webauthnKeyId,
|
||||
]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return Response::json([
|
||||
'error' => [
|
||||
'message' => trans('webauthn::errors.object_not_found'),
|
||||
],
|
||||
], 404);
|
||||
// Using vendor Facade to ensure disabled keys are included
|
||||
if (! Webauthn::hasKey(user())) {
|
||||
// Remove session value when last key is deleted
|
||||
Webauthn::logout();
|
||||
}
|
||||
|
||||
return app(DestroyResponse::class);
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
return abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,12 @@ class WebauthnEnabledKeyController extends Controller
|
||||
return response('', 201);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
public function destroy(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'current' => 'required|string|current_password',
|
||||
]);
|
||||
|
||||
$webauthnKey = user()->webauthnKeys()->findOrFail($id);
|
||||
|
||||
$webauthnKey->disable();
|
||||
|
||||
@ -10,6 +10,6 @@ class BannerLocationController extends Controller
|
||||
{
|
||||
user()->update(['banner_location' => $request->banner_location]);
|
||||
|
||||
return back()->with(['status' => 'Location Updated Successfully']);
|
||||
return back()->with(['flash' => 'Location Updated Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,11 +10,11 @@ class BrowserSessionController extends Controller
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'current_password_sesssions' => 'current_password',
|
||||
'current' => 'required|string|current_password',
|
||||
]);
|
||||
|
||||
Auth::logoutOtherDevices($request->current_password_sesssions);
|
||||
Auth::logoutOtherDevices($request->current);
|
||||
|
||||
return back()->with(['status' => 'Successfully logged out of other browser sessions!']);
|
||||
return back()->with(['flash' => 'Successfully logged out of other browser sessions!']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,6 @@ class DeactivateAliasController extends Controller
|
||||
$alias->deactivate();
|
||||
|
||||
return redirect()->route('aliases.index')
|
||||
->with(['status' => 'Alias '.$alias->email.' deactivated successfully!']);
|
||||
->with(['flash' => 'Alias '.$alias->email.' deactivated successfully!']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateDefaultAliasFormatRequest;
|
||||
|
||||
class DefaultAliasFormatController extends Controller
|
||||
{
|
||||
public function update(UpdateDefaultAliasFormatRequest $request)
|
||||
{
|
||||
user()->default_alias_format = $request->format;
|
||||
user()->save();
|
||||
|
||||
return back()->with(['status' => 'Default Alias Format Updated Successfully']);
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,6 @@ class DefaultAliasDomainController extends Controller
|
||||
user()->default_alias_domain = $request->domain;
|
||||
user()->save();
|
||||
|
||||
return back()->with(['status' => 'Default Alias Domain Updated Successfully']);
|
||||
return back()->with(['flash' => 'Default Alias Domain Updated Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,6 @@ class DefaultAliasFormatController extends Controller
|
||||
user()->default_alias_format = $request->format;
|
||||
user()->save();
|
||||
|
||||
return back()->with(['status' => 'Default Alias Format Updated Successfully']);
|
||||
return back()->with(['flash' => 'Default Alias Format Updated Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,29 +21,46 @@ class DefaultRecipientController extends Controller
|
||||
|
||||
public function update(UpdateDefaultRecipientRequest $request)
|
||||
{
|
||||
$recipient = user()->verifiedRecipients()->findOrFail($request->default_recipient);
|
||||
$recipient = user()->verifiedRecipients()->findOrFail($request->id);
|
||||
|
||||
$currentDefaultRecipient = user()->defaultRecipient;
|
||||
|
||||
user()->default_recipient = $recipient;
|
||||
user()->save();
|
||||
user()->update(['default_recipient_id' => $recipient->id]);
|
||||
|
||||
if ($currentDefaultRecipient->id !== $recipient->id) {
|
||||
$currentDefaultRecipient->notify(new DefaultRecipientUpdated($recipient->email));
|
||||
}
|
||||
|
||||
return back()->with(['status' => 'Default Recipient Updated Successfully']);
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(EditDefaultRecipientRequest $request)
|
||||
{
|
||||
$recipient = user()->defaultRecipient;
|
||||
|
||||
$recipient->email = $request->email;
|
||||
// Updating already verified default recipient, create new pending entry and send verification email.
|
||||
if ($recipient->hasVerifiedEmail()) {
|
||||
// Clear all other pending entries
|
||||
user()->pendingRecipients()->delete();
|
||||
|
||||
$pendingRecipient = user()->recipients()->create([
|
||||
'email' => strtolower($request->email),
|
||||
'pending' => true,
|
||||
]);
|
||||
|
||||
$pendingRecipient->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with(['flash' => 'Email Pending Verification, Please Check Your Inbox For The Verification Email']);
|
||||
}
|
||||
|
||||
// Unverified default recipient so we can simply update and send the verification email.
|
||||
$recipient->email = strtolower($request->email);
|
||||
$recipient->save();
|
||||
|
||||
user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with(['status' => 'Email Updated Successfully, Please Check Your Inbox For The Verification Email']);
|
||||
return back()->with(['flash' => 'Email Updated Successfully, Please Check Your Inbox For The Verification Email']);
|
||||
}
|
||||
}
|
||||
|
||||
22
app/Http/Controllers/DefaultUsernameController.php
Normal file
22
app/Http/Controllers/DefaultUsernameController.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateDefaultUsernameRequest;
|
||||
|
||||
class DefaultUsernameController extends Controller
|
||||
{
|
||||
public function update(UpdateDefaultUsernameRequest $request)
|
||||
{
|
||||
$username = user()->usernames()->findOrFail($request->id);
|
||||
|
||||
// Ensure username can be used to login
|
||||
$username->allowLogin();
|
||||
|
||||
user()->update(['default_username_id' => $username->id]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
app/Http/Controllers/DisplayFromFormatController.php
Normal file
17
app/Http/Controllers/DisplayFromFormatController.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\DisplayFromFormat;
|
||||
use App\Http\Requests\UpdateDisplayFromFormatRequest;
|
||||
|
||||
class DisplayFromFormatController extends Controller
|
||||
{
|
||||
public function update(UpdateDisplayFromFormatRequest $request)
|
||||
{
|
||||
user()->display_from_format = DisplayFromFormat::from($request->format);
|
||||
user()->save();
|
||||
|
||||
return back()->with(['flash' => 'Default Alias Format Updated Successfully']);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,6 @@ class EmailSubjectController extends Controller
|
||||
{
|
||||
user()->update(['email_subject' => $request->email_subject]);
|
||||
|
||||
return back()->with(['status' => 'Email Subject Updated Successfully']);
|
||||
return back()->with(['flash' => 'Email Subject Updated Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,14 +2,14 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateFromNameRequest;
|
||||
use App\Http\Requests\UpdateAccountFromNameRequest;
|
||||
|
||||
class FromNameController extends Controller
|
||||
{
|
||||
public function update(UpdateFromNameRequest $request)
|
||||
public function update(UpdateAccountFromNameRequest $request)
|
||||
{
|
||||
user()->update(['from_name' => $request->from_name]);
|
||||
|
||||
return back()->with(['status' => 'From Name Updated Successfully']);
|
||||
return back()->with(['flash' => 'From Name Updated Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,16 +10,12 @@ class PasswordController extends Controller
|
||||
{
|
||||
public function update(UpdatePasswordRequest $request)
|
||||
{
|
||||
if (! Hash::check($request->current, user()->password)) {
|
||||
return redirect(url()->previous().'#update-password')->withErrors(['current' => 'Current password incorrect']);
|
||||
}
|
||||
|
||||
// Log out of other sessions
|
||||
Auth::logoutOtherDevices($request->current);
|
||||
|
||||
user()->password = Hash::make($request->password);
|
||||
user()->save();
|
||||
|
||||
return back()->with(['status' => 'Password Updated Successfully']);
|
||||
return back()->with(['flash' => 'Password Updated Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,12 +3,30 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\DestroyAccountRequest;
|
||||
use App\Http\Resources\PersonalAccessTokenResource;
|
||||
use App\Jobs\DeleteAccount;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use LaravelWebauthn\Facades\Webauthn;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
public function show()
|
||||
{
|
||||
return Inertia::render('Settings/General', [
|
||||
'defaultAliasDomain' => user()->default_alias_domain,
|
||||
'defaultAliasFormat' => user()->default_alias_format,
|
||||
'displayFromFormat' => user()->display_from_format->value,
|
||||
'useReplyTo' => user()->use_reply_to,
|
||||
'storeFailedDeliveries' => user()->store_failed_deliveries,
|
||||
'fromName' => user()->from_name ?? '',
|
||||
'emailSubject' => user()->email_subject ?? '',
|
||||
'bannerLocation' => user()->banner_location,
|
||||
'domainOptions' => user()->domainOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function security(Request $request)
|
||||
{
|
||||
$twoFactor = app('pragmarx.google2fa');
|
||||
|
||||
@ -18,26 +36,48 @@ class SettingController extends Controller
|
||||
user()->two_factor_secret
|
||||
);
|
||||
|
||||
return view('settings.show', [
|
||||
'user' => user(),
|
||||
'recipientOptions' => user()->verifiedRecipients,
|
||||
'authSecret' => user()->two_factor_secret,
|
||||
'qrCode' => $qrCode,
|
||||
// User has either webauthn or TOTP 2FA enabled
|
||||
$hasTwoFactor = Webauthn::enabled(user()) || user()->two_factor_enabled;
|
||||
|
||||
return Inertia::render('Settings/Security', [
|
||||
'authSecret' => $hasTwoFactor ? null : user()->two_factor_secret,
|
||||
'qrCode' => $hasTwoFactor ? null : $qrCode,
|
||||
'regeneratedBackupCode' => $request->session()->get('regeneratedBackupCode', null),
|
||||
'backupCode' => $request->session()->get('backupCode', null),
|
||||
'twoFactorEnabled' => user()->two_factor_enabled,
|
||||
'webauthnEnabled' => Webauthn::enabled(user()),
|
||||
'initialKeys' => user()->webauthnKeys()->latest()->select(['id', 'name', 'enabled', 'created_at'])->get()->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function api()
|
||||
{
|
||||
return Inertia::render('Settings/Api', [
|
||||
'initialTokens' => PersonalAccessTokenResource::collection(user()->tokens()->select(['id', 'tokenable_id', 'name', 'created_at', 'last_used_at', 'expires_at', 'updated_at', 'created_at'])->get()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function data()
|
||||
{
|
||||
return Inertia::render('Settings/Data', [
|
||||
'totalAliasesCount' => user()->allAliases()->count(),
|
||||
'domainsCount' => user()->domains()->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function account()
|
||||
{
|
||||
return Inertia::render('Settings/Account');
|
||||
}
|
||||
|
||||
public function destroy(DestroyAccountRequest $request)
|
||||
{
|
||||
if (! Hash::check($request->current_password_delete, user()->password)) {
|
||||
return back()->withErrors(['current_password_delete' => 'Incorrect password entered']);
|
||||
}
|
||||
|
||||
DeleteAccount::dispatch(user());
|
||||
|
||||
auth()->logout();
|
||||
$request->session()->invalidate();
|
||||
|
||||
return redirect()->route('login')
|
||||
->with(['status' => 'Account deleted successfully!']);
|
||||
->with(['flash' => 'Account deleted successfully!']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,34 +2,192 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShowAliasController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$totals = user()
|
||||
->aliases()
|
||||
->withTrashed()
|
||||
->toBase()
|
||||
->selectRaw('ifnull(sum(emails_forwarded),0) as forwarded')
|
||||
->selectRaw('ifnull(sum(emails_blocked),0) as blocked')
|
||||
->selectRaw('ifnull(sum(emails_replied),0) as replies')
|
||||
->first();
|
||||
$validated = $request->validate([
|
||||
'page' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
],
|
||||
'page_size' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'in:25,50,100',
|
||||
],
|
||||
'search' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:50',
|
||||
'min:2',
|
||||
],
|
||||
'deleted' => [
|
||||
'nullable',
|
||||
'in:with,without,only',
|
||||
'string',
|
||||
],
|
||||
'active' => [
|
||||
'nullable',
|
||||
'in:true,false',
|
||||
'string',
|
||||
],
|
||||
'shared_domain' => [
|
||||
'nullable',
|
||||
'in:true,false',
|
||||
'string',
|
||||
],
|
||||
'sort' => [
|
||||
'nullable',
|
||||
'max:20',
|
||||
'min:3',
|
||||
Rule::in([
|
||||
'local_part',
|
||||
'domain',
|
||||
'email',
|
||||
'emails_forwarded',
|
||||
'emails_blocked',
|
||||
'emails_replied',
|
||||
'emails_sent',
|
||||
'active',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
'-local_part',
|
||||
'-domain',
|
||||
'-email',
|
||||
'-emails_forwarded',
|
||||
'-emails_blocked',
|
||||
'-emails_replied',
|
||||
'-emails_sent',
|
||||
'-active',
|
||||
'-created_at',
|
||||
'-updated_at',
|
||||
'-deleted_at',
|
||||
]),
|
||||
],
|
||||
'recipient' => [
|
||||
'nullable',
|
||||
'uuid',
|
||||
],
|
||||
'domain' => [
|
||||
'nullable',
|
||||
'uuid',
|
||||
],
|
||||
'username' => [
|
||||
'nullable',
|
||||
'uuid',
|
||||
],
|
||||
]);
|
||||
|
||||
return view('aliases.index', [
|
||||
'user' => user(),
|
||||
'defaultRecipientEmail' => user()->email,
|
||||
'aliases' => user()
|
||||
->aliases()
|
||||
->with([
|
||||
'recipients:id,email',
|
||||
'aliasable.defaultRecipient:id,email',
|
||||
])
|
||||
->latest()
|
||||
->get(),
|
||||
'recipients' => user()->verifiedRecipients()->select(['id', 'email'])->get(),
|
||||
'totals' => $totals,
|
||||
'domain' => user()->username.'.'.config('anonaddy.domain'),
|
||||
'domainOptions' => user()->domainOptions(),
|
||||
if ($request->has('sort')) {
|
||||
$direction = strpos($request->input('sort'), '-') === 0 ? 'desc' : 'asc';
|
||||
$sort = ltrim($request->input('sort'), '-');
|
||||
} 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', 'created_at', 'deleted_at'])
|
||||
->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) use ($sort, $direction) {
|
||||
if ($sort === 'created_at') {
|
||||
return $query->orderBy($sort, $direction);
|
||||
}
|
||||
|
||||
// Secondary order by latest first
|
||||
return $query
|
||||
->orderBy($sort, $direction)
|
||||
->orderBy('created_at', 'desc');
|
||||
}, function ($query) {
|
||||
return $query->latest();
|
||||
})
|
||||
->when($request->input('active'), function ($query, $value) {
|
||||
$active = $value === 'true' ? true : false;
|
||||
|
||||
return $query->where('active', $active);
|
||||
})
|
||||
->when($request->input('shared_domain'), function ($query, $value) {
|
||||
if ($value === 'true') {
|
||||
return $query->whereIn('domain', config('anonaddy.all_domains'));
|
||||
}
|
||||
|
||||
return $query->whereNotIn('domain', config('anonaddy.all_domains'));
|
||||
})
|
||||
->with([
|
||||
'recipients:id,email',
|
||||
'aliasable.defaultRecipient:id,email',
|
||||
]);
|
||||
|
||||
// Check if with deleted
|
||||
if ($request->deleted === 'with') {
|
||||
$aliases->withTrashed();
|
||||
}
|
||||
|
||||
if ($request->deleted === 'only') {
|
||||
$aliases->onlyTrashed();
|
||||
}
|
||||
|
||||
if (isset($validated['search'])) {
|
||||
$searchTerm = strtolower($validated['search']);
|
||||
|
||||
// Chunk aliases and build results array by passing &$results, this is for users with tens of thousands of aliases to prevent out of memory issues.
|
||||
$searchResults = collect();
|
||||
$aliases->chunk(10000, function ($chunkedAliases) use (&$searchResults, $searchTerm) {
|
||||
$searchResults = $searchResults->concat($chunkedAliases->filter(function ($alias) use ($searchTerm) {
|
||||
return Str::contains(strtolower($alias->email), $searchTerm) || Str::contains(strtolower($alias->description), $searchTerm);
|
||||
})->values());
|
||||
});
|
||||
|
||||
$aliases = $searchResults;
|
||||
}
|
||||
|
||||
$aliases = $aliases->paginate($validated['page_size'] ?? 25)->withQueryString()->onEachSide(1);
|
||||
|
||||
if ($request->has('active')) {
|
||||
$currentAliasStatus = $request->input('active') === 'true' ? 'active' : 'inactive';
|
||||
} elseif ($request->has('deleted')) {
|
||||
$currentAliasStatus = $request->input('deleted') === 'with' ? 'all' : 'deleted';
|
||||
} else {
|
||||
$currentAliasStatus = 'active_inactive';
|
||||
}
|
||||
|
||||
return Inertia::render('Aliases/Index', [
|
||||
'initialRows' => fn () => $aliases,
|
||||
'recipientOptions' => fn () => user()->verifiedRecipients()->select(['id', 'email'])->get(),
|
||||
'subdomain' => fn () => user()->username.'.'.config('anonaddy.domain'),
|
||||
'domainOptions' => fn () => user()->domainOptions(),
|
||||
'defaultAliasDomain' => fn () => user()->default_alias_domain,
|
||||
'defaultAliasFormat' => fn () => user()->default_alias_format,
|
||||
'search' => $validated['search'] ?? null,
|
||||
'initialPageSize' => isset($validated['page_size']) ? (int) $validated['page_size'] : 25,
|
||||
'sort' => $sort,
|
||||
'sortDirection' => $direction,
|
||||
'currentAliasStatus' => $currentAliasStatus,
|
||||
'sharedDomains' => user()->sharedDomainOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$alias = user()->aliases()->withTrashed()->findOrFail($id);
|
||||
|
||||
return Inertia::render('Aliases/Edit', [
|
||||
'initialAlias' => $alias->only(['id', 'user_id', 'local_part', 'extension', 'domain', 'email', 'active', 'description', 'from_name', 'deleted_at', 'updated_at']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
37
app/Http/Controllers/ShowDashboardController.php
Normal file
37
app/Http/Controllers/ShowDashboardController.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShowDashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$totals = user()
|
||||
->aliases()
|
||||
->withTrashed()
|
||||
->toBase()
|
||||
->selectRaw('ifnull(count(id),0) as total')
|
||||
->selectRaw('ifnull(sum(active=1),0) as active')
|
||||
->selectRaw('ifnull(sum(CASE WHEN active=0 AND deleted_at IS NULL THEN 1 END),0) as inactive')
|
||||
->selectRaw('ifnull(sum(CASE WHEN deleted_at IS NOT NULL THEN 1 END),0) as deleted')
|
||||
->selectRaw('ifnull(sum(emails_forwarded),0) as forwarded')
|
||||
->selectRaw('ifnull(sum(emails_blocked),0) as blocked')
|
||||
->selectRaw('ifnull(sum(emails_replied),0) as replies')
|
||||
->selectRaw('ifnull(sum(emails_sent),0) as sent')
|
||||
->first();
|
||||
|
||||
return Inertia::render('Dashboard/Index', [
|
||||
'totals' => $totals,
|
||||
'bandwidthMb' => user()->bandwidthMb,
|
||||
'bandwidthLimit' => user()->getBandwidthLimitMb(),
|
||||
'month' => now()->format('F'),
|
||||
'aliases' => user()->activeSharedDomainAliases()->count(),
|
||||
'recipients' => user()->recipients()->count(),
|
||||
'usernames' => user()->usernames()->count(),
|
||||
'domains' => user()->domains()->count(),
|
||||
'rules' => user()->rules()->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -2,17 +2,49 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShowDomainController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('domains.index', [
|
||||
'domains' => user()
|
||||
->domains()
|
||||
->with('defaultRecipient:id,email')
|
||||
->withCount('aliases')
|
||||
->latest()
|
||||
->get(),
|
||||
// Validate search query
|
||||
$validated = $request->validate([
|
||||
'search' => 'nullable|string|max:50|min:2',
|
||||
]);
|
||||
|
||||
$domains = user()
|
||||
->domains()
|
||||
->select(['id', 'user_id', 'default_recipient_id', 'domain', 'description', 'active', 'catch_all', 'domain_mx_validated_at', 'domain_sending_verified_at', 'created_at'])
|
||||
->with('defaultRecipient:id,email')
|
||||
->withCount('aliases')
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
if (isset($validated['search'])) {
|
||||
$searchTerm = strtolower($validated['search']);
|
||||
|
||||
$domains = $domains->filter(function ($domain) use ($searchTerm) {
|
||||
return Str::contains(strtolower($domain->domain), $searchTerm) || Str::contains(strtolower($domain->description), $searchTerm);
|
||||
})->values();
|
||||
}
|
||||
|
||||
return Inertia::render('Domains/Index', [
|
||||
'initialRows' => $domains,
|
||||
'recipientOptions' => user()->verifiedRecipients()->select(['id', 'email'])->get(),
|
||||
'initialAaVerify' => sha1(config('anonaddy.secret').user()->id.user()->domains->count()),
|
||||
'search' => $validated['search'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$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', 'updated_at']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,17 +2,37 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShowFailedDeliveryController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('failed_deliveries.index', [
|
||||
'failedDeliveries' => user()
|
||||
->failedDeliveries()
|
||||
->with(['recipient:id,email', 'alias:id,email'])
|
||||
->select(['alias_id', 'bounce_type', 'code', 'attempted_at', 'created_at', 'id', 'recipient_id', 'remote_mta', 'sender', 'is_stored'])
|
||||
->latest()
|
||||
->get(),
|
||||
// Validate search query
|
||||
$validated = $request->validate([
|
||||
'search' => 'nullable|string|max:50|min:2',
|
||||
]);
|
||||
|
||||
$failedDeliveries = user()
|
||||
->failedDeliveries()
|
||||
->with(['recipient:id,email', 'alias:id,email'])
|
||||
->select(['alias_id', 'email_type', 'code', 'attempted_at', 'created_at', 'id', 'user_id', 'recipient_id', 'remote_mta', 'sender', 'destination', 'is_stored'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
if (isset($validated['search'])) {
|
||||
$searchTerm = strtolower($validated['search']);
|
||||
|
||||
$failedDeliveries = $failedDeliveries->filter(function ($failedDelivery) use ($searchTerm) {
|
||||
return Str::contains(strtolower($failedDelivery->code), $searchTerm);
|
||||
})->values();
|
||||
}
|
||||
|
||||
return Inertia::render('FailedDeliveries', [
|
||||
'initialRows' => $failedDeliveries,
|
||||
'search' => $validated['search'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,31 +2,31 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShowRecipientController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$recipients = user()->recipients()->with([
|
||||
'aliases:id,aliasable_id,email',
|
||||
'domainsUsingAsDefault.aliases:id,aliasable_id,email',
|
||||
'usernamesUsingAsDefault.aliases:id,aliasable_id,email',
|
||||
])->latest()->get();
|
||||
// Validate search query
|
||||
$validated = $request->validate([
|
||||
'search' => 'nullable|string|max:50|min:2',
|
||||
]);
|
||||
|
||||
$recipients->each(function ($recipient) {
|
||||
if ($recipient->domainsUsingAsDefault) {
|
||||
$domainAliases = $recipient->domainsUsingAsDefault->flatMap(function ($domain) {
|
||||
return $domain->aliases;
|
||||
});
|
||||
$recipient->setRelation('aliases', $recipient->aliases->concat($domainAliases)->unique('email'));
|
||||
}
|
||||
|
||||
if ($recipient->usernamesUsingAsDefault) {
|
||||
$usernameAliases = $recipient->usernamesUsingAsDefault->flatMap(function ($domain) {
|
||||
return $domain->aliases;
|
||||
});
|
||||
$recipient->setRelation('aliases', $recipient->aliases->concat($usernameAliases)->unique('email'));
|
||||
}
|
||||
});
|
||||
$recipients = user()->recipients()
|
||||
->select([
|
||||
'id',
|
||||
'user_id',
|
||||
'email',
|
||||
'should_encrypt',
|
||||
'fingerprint',
|
||||
'email_verified_at',
|
||||
'created_at',
|
||||
])
|
||||
->latest()->get();
|
||||
|
||||
$count = $recipients->count();
|
||||
|
||||
@ -34,11 +34,55 @@ class ShowRecipientController extends Controller
|
||||
$item['key'] = $count - $key;
|
||||
});
|
||||
|
||||
return view('recipients.index', [
|
||||
'recipients' => $recipients,
|
||||
'aliasesUsingDefault' => user()->aliasesUsingDefault()->take(5)->get(),
|
||||
'aliasesUsingDefaultCount' => user()->aliasesUsingDefault()->count(),
|
||||
'user' => user()->load('defaultUsername'),
|
||||
if (isset($validated['search'])) {
|
||||
$searchTerm = strtolower($validated['search']);
|
||||
|
||||
$recipients = $recipients->filter(function ($recipient) use ($searchTerm) {
|
||||
return Str::contains(strtolower($recipient->email), $searchTerm);
|
||||
})->values();
|
||||
}
|
||||
|
||||
return Inertia::render('Recipients/Index', [
|
||||
'initialRows' => $recipients,
|
||||
//'aliasesUsingDefaultCount' => user()->aliasesUsingDefaultCount(),
|
||||
'search' => $validated['search'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function aliasCount(Request $request)
|
||||
{
|
||||
// Validate search query
|
||||
$validated = $request->validate([
|
||||
'ids' => 'required|array|max:30|min:1',
|
||||
'ids.*' => 'required|uuid|distinct',
|
||||
]);
|
||||
|
||||
$count = user()->recipients()
|
||||
->whereIn('id', $validated['ids'])
|
||||
->select([
|
||||
'id',
|
||||
'user_id',
|
||||
])->withCount([
|
||||
'aliases',
|
||||
'domainAliasesUsingAsDefault' => function (Builder $query) {
|
||||
$query->doesntHave('recipients');
|
||||
},
|
||||
'usernameAliasesUsingAsDefault' => function (Builder $query) {
|
||||
$query->doesntHave('recipients');
|
||||
},
|
||||
])->latest()->get(); // Must order by the same to ensure keys match
|
||||
|
||||
return response()->json([
|
||||
'count' => $count,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$recipient = user()->recipients()->findOrFail($id);
|
||||
|
||||
return Inertia::render('Recipients/Edit', [
|
||||
'initialRecipient' => $recipient->only(['id', 'user_id', 'email', 'can_reply_send', 'fingerprint', 'protected_headers', 'inline_encryption', 'email_verified_at', 'updated_at']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,15 +2,27 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShowRuleController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('rules.index', [
|
||||
'rules' => user()
|
||||
// Validate search query
|
||||
$validated = $request->validate([
|
||||
'search' => 'nullable|string|max:50|min:2',
|
||||
]);
|
||||
|
||||
return Inertia::render('Rules', [
|
||||
'initialRows' => user()
|
||||
->rules()
|
||||
->when($request->input('search'), function ($query, $search) {
|
||||
return $query->where('name', 'like', '%'.$search.'%');
|
||||
})
|
||||
->orderBy('order')
|
||||
->get(),
|
||||
'search' => $validated['search'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,17 +2,49 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShowUsernameController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('usernames.index', [
|
||||
'usernames' => user()
|
||||
->usernames()
|
||||
->with('defaultRecipient:id,email')
|
||||
->withCount('aliases')
|
||||
->latest()
|
||||
->get(),
|
||||
// Validate search query
|
||||
$validated = $request->validate([
|
||||
'search' => 'nullable|string|max:50|min:2',
|
||||
]);
|
||||
|
||||
$usernames = user()
|
||||
->usernames()
|
||||
->select(['id', 'user_id', 'default_recipient_id', 'username', 'description', 'active', 'catch_all', 'created_at'])
|
||||
->with('defaultRecipient:id,email')
|
||||
->withCount('aliases')
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
if (isset($validated['search'])) {
|
||||
$searchTerm = strtolower($validated['search']);
|
||||
|
||||
$usernames = $usernames->filter(function ($username) use ($searchTerm) {
|
||||
return Str::contains(strtolower($username->username), $searchTerm) || Str::contains(strtolower($username->description), $searchTerm);
|
||||
})->values();
|
||||
}
|
||||
|
||||
return Inertia::render('Usernames/Index', [
|
||||
'initialRows' => $usernames,
|
||||
'recipientOptions' => user()->verifiedRecipients()->select(['id', 'email'])->get(),
|
||||
'search' => $validated['search'] ?? null,
|
||||
'usernameCount' => (int) config('anonaddy.additional_username_limit'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$username = user()->usernames()->findOrFail($id);
|
||||
|
||||
return Inertia::render('Usernames/Edit', [
|
||||
'initialUsername' => $username->only(['id', 'user_id', 'username', 'description', 'from_name', 'can_login', 'updated_at']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,6 @@ class StoreFailedDeliveryController extends Controller
|
||||
user()->update(['store_failed_deliveries' => false]);
|
||||
}
|
||||
|
||||
return back()->with(['status' => $request->store_failed_deliveries ? 'Store Failed Deliveries Enabled Successfully' : 'Store Failed Deliveries Disabled Successfully']);
|
||||
return back()->with(['flash' => $request->store_failed_deliveries ? 'Store Failed Deliveries Enabled Successfully' : 'Store Failed Deliveries Disabled Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,6 @@ class UseReplyToController extends Controller
|
||||
user()->update(['use_reply_to' => false]);
|
||||
}
|
||||
|
||||
return back()->with(['status' => $request->use_reply_to ? 'Use Reply To Enabled Successfully' : 'Use Reply To Disabled Successfully']);
|
||||
return back()->with(['flash' => $request->use_reply_to ? 'Use Reply To Enabled Successfully' : 'Use Reply To Disabled Successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ class Kernel extends HttpKernel
|
||||
\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' => [
|
||||
|
||||
62
app/Http/Middleware/HandleInertiaRequests.php
Normal file
62
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Helpers\GitVersionHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'layouts.app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
return array_merge(parent::share($request), [
|
||||
'flash' => $request->session()->get('flash', null),
|
||||
'user' => function () use ($request) {
|
||||
if (! $request->user()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
return [
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'default_recipient_id' => $user->default_recipient_id,
|
||||
'default_username_id' => $user->default_username_id,
|
||||
];
|
||||
},
|
||||
'errorBags' => function () {
|
||||
return collect(optional(Session::get('errors'))->getBags() ?: [])->mapWithKeys(function ($bag, $key) {
|
||||
return [$key => $bag->messages()];
|
||||
})->all();
|
||||
},
|
||||
'version' => GitVersionHelper::version(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,7 @@ class DestroyAccountRequest extends FormRequest
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'current_password_delete' => 'required|string',
|
||||
'password' => 'required|string|current_password',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,10 +29,10 @@ class EditDefaultRecipientRequest extends FormRequest
|
||||
'required',
|
||||
'email:rfc,dns',
|
||||
'max:254',
|
||||
'confirmed',
|
||||
new RegisterUniqueRecipient(),
|
||||
'not_in:'.$this->user()->email,
|
||||
],
|
||||
'current' => 'required|string|current_password',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ class EnableTwoFactorAuthRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'two_factor_token' => 'required|min:6',
|
||||
'current' => 'required|string|current_password',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ namespace App\Http\Requests;
|
||||
use App\Rules\ValidAliasLocalPart;
|
||||
use App\Rules\VerifiedRecipientId;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreAliasRequest extends FormRequest
|
||||
@ -26,6 +27,7 @@ class StoreAliasRequest extends FormRequest
|
||||
{
|
||||
$this->merge([
|
||||
'domain' => strtolower($this->domain),
|
||||
'local_part_without_extension' => Str::before($this->local_part, '+'), // Remove extension so that we can check alias uniqueness properly
|
||||
]);
|
||||
}
|
||||
|
||||
@ -55,12 +57,11 @@ class StoreAliasRequest extends FormRequest
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->sometimes('local_part', [
|
||||
$validator->sometimes('local_part_without_extension', [
|
||||
'required',
|
||||
'max:50',
|
||||
Rule::unique('aliases')->where(function ($query) {
|
||||
return $query->where('local_part', $this->validationData()['local_part'])
|
||||
->where('domain', $this->validationData()['domain']);
|
||||
Rule::unique('aliases', 'local_part')->where(function ($query) {
|
||||
return $query->where('domain', $this->validationData()['domain']);
|
||||
}),
|
||||
new ValidAliasLocalPart(),
|
||||
], function () {
|
||||
@ -73,7 +74,7 @@ class StoreAliasRequest extends FormRequest
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'local_part.unique' => 'That alias already exists.',
|
||||
'local_part_without_extension.unique' => 'That alias already exists.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +35,10 @@ class StorePersonalAccessTokenRequest extends FormRequest
|
||||
'max:5',
|
||||
'in:day,week,month,year',
|
||||
],
|
||||
'password' => [
|
||||
'required',
|
||||
'string',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateFromNameRequest extends FormRequest
|
||||
class UpdateAccountFromNameRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
@ -25,6 +25,7 @@ class UpdateAliasRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'description' => 'nullable|max:200',
|
||||
'from_name' => 'nullable|string|max:50',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ class UpdateDefaultRecipientRequest extends FormRequest
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'default_recipient' => 'required|string',
|
||||
'id' => 'required|uuid',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
28
app/Http/Requests/UpdateDefaultUsernameRequest.php
Normal file
28
app/Http/Requests/UpdateDefaultUsernameRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateDefaultUsernameRequest 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|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'id' => 'required|uuid',
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Http/Requests/UpdateDisplayFromFormatRequest.php
Normal file
34
app/Http/Requests/UpdateDisplayFromFormatRequest.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\DisplayFromFormat;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateDisplayFromFormatRequest 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|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'format' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::in(array_column(DisplayFromFormat::cases(), 'value')),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ class UpdateDomainRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'description' => 'nullable|max:200',
|
||||
'from_name' => 'nullable|string|max:50',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class UpdatePasswordRequest extends FormRequest
|
||||
{
|
||||
@ -24,8 +25,12 @@ class UpdatePasswordRequest extends FormRequest
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'current' => 'required|string',
|
||||
'password' => 'required|confirmed|min:8',
|
||||
'current' => 'required|string|current_password',
|
||||
'password' => [
|
||||
'required',
|
||||
'confirmed',
|
||||
Password::defaults(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ class UpdateUsernameRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'description' => 'nullable|max:200',
|
||||
'from_name' => 'nullable|string|max:50',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ class AliasResource extends JsonResource
|
||||
'email' => $this->email,
|
||||
'active' => $this->active,
|
||||
'description' => $this->description,
|
||||
'from_name' => $this->from_name,
|
||||
'emails_forwarded' => $this->emails_forwarded,
|
||||
'emails_blocked' => $this->emails_blocked,
|
||||
'emails_replied' => $this->emails_replied,
|
||||
|
||||
@ -13,6 +13,7 @@ class DomainResource extends JsonResource
|
||||
'user_id' => $this->user_id,
|
||||
'domain' => $this->domain,
|
||||
'description' => $this->description,
|
||||
'from_name' => $this->from_name,
|
||||
'aliases' => AliasResource::collection($this->whenLoaded('aliases')),
|
||||
'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
|
||||
'active' => $this->active,
|
||||
|
||||
@ -33,6 +33,7 @@ class UserResource extends JsonResource
|
||||
'recipient_count' => $this->recipients()->count(),
|
||||
'active_domain_count' => $this->domains()->where('active', true)->count(),
|
||||
'active_shared_domain_alias_count' => $this->activeSharedDomainAliases()->count(),
|
||||
'active_rule_count' => $this->activeRules()->count(),
|
||||
'total_emails_forwarded' => (int) $totals->forwarded,
|
||||
'total_emails_blocked' => (int) $totals->blocked,
|
||||
'total_emails_replied' => (int) $totals->replied,
|
||||
|
||||
@ -13,10 +13,12 @@ class UsernameResource extends JsonResource
|
||||
'user_id' => $this->user_id,
|
||||
'username' => $this->username,
|
||||
'description' => $this->description,
|
||||
'from_name' => $this->from_name,
|
||||
'aliases' => AliasResource::collection($this->whenLoaded('aliases')),
|
||||
'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
|
||||
'active' => $this->active,
|
||||
'catch_all' => $this->catch_all,
|
||||
'can_login' => $this->can_login,
|
||||
'created_at' => $this->created_at->toDateTimeString(),
|
||||
'updated_at' => $this->updated_at->toDateTimeString(),
|
||||
];
|
||||
|
||||
24
app/Http/Responses/RegisterSuccessResponse.php
Normal file
24
app/Http/Responses/RegisterSuccessResponse.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Responses;
|
||||
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\Support\Str;
|
||||
use LaravelWebauthn\Http\Responses\RegisterSuccessResponse as RegisterSuccessResponseBase;
|
||||
|
||||
class RegisterSuccessResponse extends RegisterSuccessResponseBase
|
||||
{
|
||||
public function toResponse($request)
|
||||
{
|
||||
// If the user already has at least one key do not generate a new backup code.
|
||||
if (user()->webauthnKeys()->count() > 1) {
|
||||
return Redirect::intended('/settings/security');
|
||||
}
|
||||
|
||||
user()->update([
|
||||
'two_factor_backup_code' => bcrypt($code = Str::random(40)),
|
||||
]);
|
||||
|
||||
return Redirect::intended('/settings/security')->with(['backupCode' => $code]);
|
||||
}
|
||||
}
|
||||
@ -30,9 +30,9 @@ use Maatwebsite\Excel\Events\AfterSheet;
|
||||
use Maatwebsite\Excel\Events\ImportFailed;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class AliasesImport implements ToModel, WithHeadingRow, WithValidation, WithChunkReading, ShouldQueue, SkipsOnFailure, SkipsEmptyRows, SkipsOnError, WithLimit, WithColumnLimit, WithEvents
|
||||
class AliasesImport implements ShouldQueue, SkipsEmptyRows, SkipsOnError, SkipsOnFailure, ToModel, WithChunkReading, WithColumnLimit, WithEvents, WithHeadingRow, WithLimit, WithValidation
|
||||
{
|
||||
use Queueable, Importable, SkipsFailures, SkipsErrors, RemembersRowNumber;
|
||||
use Importable, Queueable, RemembersRowNumber, SkipsErrors, SkipsFailures;
|
||||
|
||||
protected $user;
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class DeleteAccount implements ShouldQueue, ShouldBeEncrypted
|
||||
class DeleteAccount implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Mail;
|
||||
|
||||
use App\CustomMailDriver\Mime\Part\InlineImagePart;
|
||||
use App\Enums\DisplayFromFormat;
|
||||
use App\Models\Alias;
|
||||
use App\Models\EmailData;
|
||||
use App\Models\Recipient;
|
||||
@ -17,11 +18,11 @@ use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use CheckUserRules;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
use CheckUserRules;
|
||||
|
||||
protected $email;
|
||||
|
||||
@ -41,6 +42,8 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
protected $emailSubject;
|
||||
|
||||
protected $replacedSubject;
|
||||
|
||||
protected $emailText;
|
||||
|
||||
protected $emailHtml;
|
||||
@ -55,6 +58,8 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
protected $bannerLocationHtml;
|
||||
|
||||
protected $isSpam;
|
||||
|
||||
protected $fingerprint;
|
||||
|
||||
protected $encryptedParts;
|
||||
@ -85,12 +90,14 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
protected $recipientId;
|
||||
|
||||
protected $verpDomain;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Alias $alias, EmailData $emailData, Recipient $recipient)
|
||||
public function __construct(Alias $alias, EmailData $emailData, Recipient $recipient, $isSpam = false)
|
||||
{
|
||||
$this->user = $alias->user;
|
||||
$this->alias = $alias;
|
||||
@ -122,6 +129,7 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->fingerprint = $recipient->should_encrypt && ! $this->isAlreadyEncrypted() ? $recipient->fingerprint : null;
|
||||
|
||||
$this->bannerLocationText = $this->bannerLocationHtml = $this->isAlreadyEncrypted() ? 'off' : $this->alias->user->banner_location;
|
||||
$this->isSpam = $isSpam;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,8 +148,6 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->fromEmail = $this->alias->local_part.'+'.Str::replaceLast('@', '=', $this->replyToAddress).'@'.$this->alias->domain;
|
||||
}
|
||||
|
||||
$returnPath = $this->alias->email;
|
||||
|
||||
if ($this->alias->isCustomDomain()) {
|
||||
if (! $this->alias->aliasable->isVerifiedForSending()) {
|
||||
if (! isset($replyToEmail)) {
|
||||
@ -149,15 +155,22 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
|
||||
$this->fromEmail = config('mail.from.address');
|
||||
$returnPath = config('anonaddy.return_path');
|
||||
$this->verpDomain = config('anonaddy.domain');
|
||||
}
|
||||
}
|
||||
|
||||
$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, base64_decode($this->displayFrom)." '".$this->sender."'")
|
||||
->from($this->fromEmail, $displayFrom)
|
||||
->subject($this->user->email_subject ?? base64_decode($this->emailSubject))
|
||||
->withSymfonyMessage(function (Email $message) use ($returnPath) {
|
||||
$message->returnPath($returnPath);
|
||||
->withSymfonyMessage(function (Email $message) {
|
||||
|
||||
$message->getHeaders()
|
||||
->addTextHeader('Feedback-ID', 'F:'.$this->alias->id.':anonaddy');
|
||||
@ -238,7 +251,7 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$part->setContentId(base64_decode($attachment['contentId']));
|
||||
$part->setFileName(base64_decode($attachment['file_name']));
|
||||
|
||||
$message->attachPart($part);
|
||||
$message->addPart($part);
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,6 +281,16 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
]);
|
||||
}
|
||||
|
||||
// No HTML content but isSpam, then force html version
|
||||
if (! $this->emailHtml && $this->isSpam) {
|
||||
// Turn off the banner for the plain text version
|
||||
$this->bannerLocationText = 'off';
|
||||
|
||||
$this->email->view('emails.forward.html')->with([
|
||||
'html' => base64_decode($this->emailText),
|
||||
]);
|
||||
}
|
||||
|
||||
// To prevent invalid view error where no text or html is present...
|
||||
if (! $this->emailHtml && ! $this->emailText) {
|
||||
$this->email->text('emails.forward.text')->with([
|
||||
@ -290,17 +313,22 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->email->with([
|
||||
'locationText' => $this->bannerLocationText,
|
||||
'locationHtml' => $this->bannerLocationHtml,
|
||||
'isSpam' => $this->isSpam,
|
||||
'deactivateUrl' => $this->deactivateUrl,
|
||||
'aliasEmail' => $this->alias->email,
|
||||
'aliasDomain' => $this->alias->domain,
|
||||
'aliasDescription' => $this->alias->description,
|
||||
'userId' => $this->user->id,
|
||||
'aliasId' => $this->alias->id,
|
||||
'recipientId' => $this->recipientId,
|
||||
'emailType' => 'F',
|
||||
'fingerprint' => $this->fingerprint,
|
||||
'encryptedParts' => $this->encryptedParts,
|
||||
'fromEmail' => $this->sender,
|
||||
'replacedSubject' => $this->replacedSubject,
|
||||
'shouldBlock' => $this->size === 0,
|
||||
'needsDkimSignature' => $this->needsDkimSignature(),
|
||||
'verpDomain' => $this->verpDomain ?? $this->alias->domain,
|
||||
]);
|
||||
|
||||
if (isset($replyToEmail)) {
|
||||
@ -354,6 +382,21 @@ class ForwardEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
]);
|
||||
}
|
||||
|
||||
private function getUserDisplayFrom($displayFrom)
|
||||
{
|
||||
// Check user display_from_format settings and then return correct format
|
||||
return match ($this->user->display_from_format) {
|
||||
DisplayFromFormat::DEFAULT => str_replace('@', ' at ', $displayFrom." '".$this->sender."'"),
|
||||
DisplayFromFormat::BRACKETS => str_replace('@', '(at)', $displayFrom.' - '.$this->sender),
|
||||
DisplayFromFormat::DOMAIN => str_replace('@', ' at ', $displayFrom.' - '.Str::afterLast($this->sender, '@')),
|
||||
DisplayFromFormat::NAME => str_replace('@', ' at ', $displayFrom),
|
||||
DisplayFromFormat::ADDRESS => str_replace('@', ' at ', $this->sender),
|
||||
DisplayFromFormat::DOMAINONLY => Str::afterLast($this->sender, '@'),
|
||||
DisplayFromFormat::NONE => null,
|
||||
default => str_replace('@', ' at ', $displayFrom." '".$this->sender."'"),
|
||||
};
|
||||
}
|
||||
|
||||
private function isAlreadyEncrypted()
|
||||
{
|
||||
return $this->encryptedParts || preg_match('/^-----BEGIN PGP MESSAGE-----([A-Za-z0-9+=\/\n]+)-----END PGP MESSAGE-----$/', base64_decode($this->emailText));
|
||||
|
||||
@ -16,11 +16,11 @@ use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use CheckUserRules;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
use CheckUserRules;
|
||||
|
||||
protected $email;
|
||||
|
||||
@ -52,6 +52,8 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
protected $references;
|
||||
|
||||
protected $verpDomain;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
@ -68,7 +70,7 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->emailAttachments = $emailData->attachments;
|
||||
$this->emailInlineAttachments = $emailData->inlineAttachments;
|
||||
$this->encryptedParts = $emailData->encryptedParts ?? null;
|
||||
$this->displayFrom = $user->from_name ?? null;
|
||||
$this->displayFrom = $alias->getFromName();
|
||||
$this->size = $emailData->size;
|
||||
$this->inReplyTo = $emailData->inReplyTo;
|
||||
$this->references = $emailData->references;
|
||||
@ -81,21 +83,19 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$returnPath = $this->alias->email;
|
||||
$this->fromEmail = $this->alias->email;
|
||||
|
||||
if ($this->alias->isCustomDomain()) {
|
||||
if (! $this->alias->aliasable->isVerifiedForSending()) {
|
||||
$this->fromEmail = config('mail.from.address');
|
||||
$returnPath = config('anonaddy.return_path');
|
||||
$this->verpDomain = config('anonaddy.domain');
|
||||
}
|
||||
}
|
||||
|
||||
$this->email = $this
|
||||
->from($this->fromEmail, $this->displayFrom)
|
||||
->subject(base64_decode($this->emailSubject))
|
||||
->withSymfonyMessage(function (Email $message) use ($returnPath) {
|
||||
$message->returnPath($returnPath);
|
||||
->withSymfonyMessage(function (Email $message) {
|
||||
|
||||
$message->getHeaders()
|
||||
->addTextHeader('Feedback-ID', 'R:'.$this->alias->id.':anonaddy');
|
||||
@ -124,7 +124,7 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$part->setContentId(base64_decode($attachment['contentId']));
|
||||
$part->setFileName(base64_decode($attachment['file_name']));
|
||||
|
||||
$message->attachPart($part);
|
||||
$message->addPart($part);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -159,10 +159,14 @@ class ReplyToEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->checkRules('Replies');
|
||||
|
||||
$this->email->with([
|
||||
'userId' => $this->user->id,
|
||||
'aliasId' => $this->alias->id,
|
||||
'emailType' => 'R',
|
||||
'shouldBlock' => $this->size === 0,
|
||||
'encryptedParts' => $this->encryptedParts,
|
||||
'needsDkimSignature' => $this->needsDkimSignature(),
|
||||
'aliasDomain' => $this->alias->domain,
|
||||
'verpDomain' => $this->verpDomain ?? $this->alias->domain,
|
||||
]);
|
||||
|
||||
if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
|
||||
|
||||
@ -16,11 +16,11 @@ use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use CheckUserRules;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
use CheckUserRules;
|
||||
|
||||
protected $email;
|
||||
|
||||
@ -48,6 +48,8 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
protected $size;
|
||||
|
||||
protected $verpDomain;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
@ -64,7 +66,7 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->emailAttachments = $emailData->attachments;
|
||||
$this->emailInlineAttachments = $emailData->inlineAttachments;
|
||||
$this->encryptedParts = $emailData->encryptedParts ?? null;
|
||||
$this->displayFrom = $user->from_name ?? null;
|
||||
$this->displayFrom = $alias->getFromName();
|
||||
$this->size = $emailData->size;
|
||||
}
|
||||
|
||||
@ -75,21 +77,19 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$returnPath = $this->alias->email;
|
||||
$this->fromEmail = $this->alias->email;
|
||||
|
||||
if ($this->alias->isCustomDomain()) {
|
||||
if (! $this->alias->aliasable->isVerifiedForSending()) {
|
||||
$this->fromEmail = config('mail.from.address');
|
||||
$returnPath = config('anonaddy.return_path');
|
||||
$this->verpDomain = config('anonaddy.domain');
|
||||
}
|
||||
}
|
||||
|
||||
$this->email = $this
|
||||
->from($this->fromEmail, $this->displayFrom)
|
||||
->subject(base64_decode($this->emailSubject))
|
||||
->withSymfonyMessage(function (Email $message) use ($returnPath) {
|
||||
$message->returnPath($returnPath);
|
||||
->withSymfonyMessage(function (Email $message) {
|
||||
|
||||
$message->getHeaders()
|
||||
->addTextHeader('Feedback-ID', 'S:'.$this->alias->id.':anonaddy');
|
||||
@ -108,7 +108,7 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$part->setContentId(base64_decode($attachment['contentId']));
|
||||
$part->setFileName(base64_decode($attachment['file_name']));
|
||||
|
||||
$message->attachPart($part);
|
||||
$message->addPart($part);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -143,10 +143,14 @@ class SendFromEmail extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->checkRules('Sends');
|
||||
|
||||
$this->email->with([
|
||||
'userId' => $this->user->id,
|
||||
'aliasId' => $this->alias->id,
|
||||
'emailType' => 'S',
|
||||
'shouldBlock' => $this->size === 0,
|
||||
'encryptedParts' => $this->encryptedParts,
|
||||
'needsDkimSignature' => $this->needsDkimSignature(),
|
||||
'aliasDomain' => $this->alias->domain,
|
||||
'verpDomain' => $this->verpDomain ?? $this->alias->domain,
|
||||
]);
|
||||
|
||||
if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
|
||||
|
||||
@ -10,7 +10,7 @@ use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class TokenExpiringSoon extends Mailable implements ShouldQueue, ShouldBeEncrypted
|
||||
class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
@ -38,10 +38,12 @@ class TokenExpiringSoon extends Mailable implements ShouldQueue, ShouldBeEncrypt
|
||||
public function build()
|
||||
{
|
||||
return $this
|
||||
->subject('Your AnonAddy API token expires soon')
|
||||
->subject('Your addy.io API key expires soon')
|
||||
->markdown('mail.token_expiring_soon', [
|
||||
'user' => $this->user,
|
||||
'recipientId' => $this->recipient->id,
|
||||
'userId' => $this->user->id,
|
||||
'recipientId' => $this->user->default_recipient_id,
|
||||
'emailType' => 'TES',
|
||||
'fingerprint' => $this->recipient->should_encrypt ? $this->recipient->fingerprint : null,
|
||||
])
|
||||
->withSymfonyMessage(function (Email $message) {
|
||||
|
||||
@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Traits\HasEncryptedAttributes;
|
||||
use App\Traits\HasUuid;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -12,10 +13,10 @@ use Illuminate\Support\Str;
|
||||
|
||||
class Alias extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use HasUuid;
|
||||
use HasEncryptedAttributes;
|
||||
use HasFactory;
|
||||
use HasUuid;
|
||||
use SoftDeletes;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
@ -23,6 +24,7 @@ class Alias extends Model
|
||||
|
||||
protected $encrypted = [
|
||||
'description',
|
||||
'from_name',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
@ -30,6 +32,7 @@ class Alias extends Model
|
||||
'user_id',
|
||||
'active',
|
||||
'description',
|
||||
'from_name',
|
||||
'email',
|
||||
'local_part',
|
||||
'extension',
|
||||
@ -123,6 +126,14 @@ class Alias extends Model
|
||||
return $this->hasMany(FailedDelivery::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the aliases' outbound messages.
|
||||
*/
|
||||
public function outboundMessages()
|
||||
{
|
||||
return $this->hasMany(OutboundMessage::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the verified recipients for the email alias.
|
||||
*/
|
||||
@ -154,6 +165,24 @@ class Alias extends Model
|
||||
return $verifiedRecipients;
|
||||
}
|
||||
|
||||
public function scopeUsesRecipientWithId($query, $id, $isDefault = false)
|
||||
{
|
||||
return $query->where(function (Builder $q) use ($id) {
|
||||
return $q->whereHas('recipients', function (Builder $query) use ($id) {
|
||||
$query->where('recipients.id', $id);
|
||||
})->orWhere(function (Builder $q) use ($id) {
|
||||
return $q->whereHasMorph('aliasable', ['App\Models\Domain', 'App\Models\Username'], function (Builder $query) use ($id) {
|
||||
$query->where('default_recipient_id', $id);
|
||||
})->doesntHave('recipients');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeBelongsToAliasable($query, $type, $id)
|
||||
{
|
||||
return $query->where('aliasable_type', $type)->where('aliasable_id', $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the alias.
|
||||
*/
|
||||
@ -180,6 +209,22 @@ class Alias extends Model
|
||||
return in_array($this->domain, config('anonaddy.all_domains'));
|
||||
}
|
||||
|
||||
public function getFromName()
|
||||
{
|
||||
// Check alias from name
|
||||
if ($aliasFromName = $this->from_name) {
|
||||
return $aliasFromName;
|
||||
}
|
||||
|
||||
// Check username / custom domain from name
|
||||
if ($aliasableFromName = $this->aliasable?->from_name) {
|
||||
return $aliasableFromName;
|
||||
}
|
||||
|
||||
// Check user settings global from name
|
||||
return $this->user->from_name ?? null;
|
||||
}
|
||||
|
||||
public function isCustomDomain()
|
||||
{
|
||||
return $this->aliasable_type === 'App\Models\Domain';
|
||||
|
||||
@ -14,9 +14,9 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Domain extends Model
|
||||
{
|
||||
use HasUuid;
|
||||
use HasEncryptedAttributes;
|
||||
use HasFactory;
|
||||
use HasUuid;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
@ -24,11 +24,13 @@ class Domain extends Model
|
||||
|
||||
protected $encrypted = [
|
||||
'description',
|
||||
'from_name',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'domain',
|
||||
'description',
|
||||
'from_name',
|
||||
'active',
|
||||
'catch_all',
|
||||
];
|
||||
|
||||
@ -64,7 +64,7 @@ class EmailData
|
||||
$this->text = base64_encode(stream_get_contents($attachment->getStream()));
|
||||
} else {
|
||||
if (! str_contains($contentType, '/')) {
|
||||
if (null === self::$mimeTypes) {
|
||||
if (self::$mimeTypes === null) {
|
||||
self::$mimeTypes = new MimeTypes();
|
||||
}
|
||||
$contentType = self::$mimeTypes->getMimeTypes($contentType)[0] ?? 'application/octet-stream';
|
||||
|
||||
@ -5,15 +5,16 @@ namespace App\Models;
|
||||
use App\Traits\HasEncryptedAttributes;
|
||||
use App\Traits\HasUuid;
|
||||
use DateTimeInterface;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FailedDelivery extends Model
|
||||
{
|
||||
use HasUuid;
|
||||
use HasEncryptedAttributes;
|
||||
use HasFactory;
|
||||
use HasUuid;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
@ -21,6 +22,7 @@ class FailedDelivery extends Model
|
||||
|
||||
protected $encrypted = [
|
||||
'sender',
|
||||
'destination',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
@ -32,6 +34,7 @@ class FailedDelivery extends Model
|
||||
'bounce_type',
|
||||
'remote_mta',
|
||||
'sender',
|
||||
'destination',
|
||||
'email_type',
|
||||
'status',
|
||||
'code',
|
||||
@ -70,6 +73,40 @@ class FailedDelivery extends Model
|
||||
return $date->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the human readable email type.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
protected function emailType(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (?string $value) => match ($value) {
|
||||
'F' => 'Forward',
|
||||
'R' => 'Reply',
|
||||
'S' => 'Send',
|
||||
'RP' => 'Reset Password',
|
||||
'FDN' => 'Failed Delivery',
|
||||
'DMI' => 'Domain MX Invalid',
|
||||
'DRU' => 'Default Recipient Updated',
|
||||
'FLA' => 'Failed Login Attempt',
|
||||
'TES' => 'Token Expiring Soon',
|
||||
'UR' => 'Username Reminder',
|
||||
'VR' => 'Verify Recipient',
|
||||
'VU' => 'Verify User',
|
||||
'DRSA' => 'Disallowed Reply/Send Attempt',
|
||||
'DUS' => 'Domain Unverified For Sending',
|
||||
'GKE' => 'PGP Key Expired',
|
||||
'NBL' => 'Near Bandwidth Limit',
|
||||
'RSL' => 'Reached Reply/Send Limit',
|
||||
'SRSA' => 'Spam Reply/Send Attempt',
|
||||
'AIF' => 'Aliases Import Finished',
|
||||
default => 'Forward',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user for the failed delivery.
|
||||
*/
|
||||
|
||||
63
app/Models/OutboundMessage.php
Normal file
63
app/Models/OutboundMessage.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OutboundMessage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'user_id',
|
||||
'alias_id',
|
||||
'recipient_id',
|
||||
'email_type',
|
||||
'bounced',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'string',
|
||||
'user_id' => 'string',
|
||||
'alias_id' => 'string',
|
||||
'recipient_id' => 'string',
|
||||
'bounced' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the user for the outbound message.
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recipient for the outbound message.
|
||||
*/
|
||||
public function recipient()
|
||||
{
|
||||
return $this->belongsTo(Recipient::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the alias for the outbound message.
|
||||
*/
|
||||
public function alias()
|
||||
{
|
||||
return $this->belongsTo(Alias::class);
|
||||
}
|
||||
|
||||
public function markAsBounced()
|
||||
{
|
||||
$this->update(['bounced' => true]);
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasUuid;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PostfixQueueId extends Model
|
||||
{
|
||||
use HasUuid;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'queue_id',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'string',
|
||||
'queue_id' => 'string',
|
||||
];
|
||||
}
|
||||
@ -6,16 +6,17 @@ use App\Notifications\CustomVerifyEmail;
|
||||
use App\Notifications\UsernameReminder;
|
||||
use App\Traits\HasEncryptedAttributes;
|
||||
use App\Traits\HasUuid;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class Recipient extends Model
|
||||
{
|
||||
use Notifiable;
|
||||
use HasUuid;
|
||||
use HasEncryptedAttributes;
|
||||
use HasFactory;
|
||||
use HasUuid;
|
||||
use Notifiable;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
@ -34,6 +35,7 @@ class Recipient extends Model
|
||||
'inline_encryption',
|
||||
'protected_headers',
|
||||
'fingerprint',
|
||||
'pending',
|
||||
'email_verified_at',
|
||||
];
|
||||
|
||||
@ -44,6 +46,7 @@ class Recipient extends Model
|
||||
'should_encrypt' => 'boolean',
|
||||
'inline_encryption' => 'boolean',
|
||||
'protected_headers' => 'boolean',
|
||||
'pending' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'email_verified_at' => 'datetime',
|
||||
@ -62,6 +65,33 @@ class Recipient extends Model
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The "booted" method of the model.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Global scope on Recipient model to not return any pending new email entries by default
|
||||
static::addGlobalScope('notPending', function (Builder $builder) {
|
||||
$builder->where('pending', false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to include pending new email recipients.
|
||||
*/
|
||||
public function scopeWithPending(Builder $query): void
|
||||
{
|
||||
$query->withoutGlobalScope('notPending');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to get only pending email recipients.
|
||||
*/
|
||||
public function scopePending(Builder $query): void
|
||||
{
|
||||
$query->withoutGlobalScope('notPending')->where('pending', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query scope to return verified or unverified recipients.
|
||||
*/
|
||||
@ -98,6 +128,14 @@ class Recipient extends Model
|
||||
return $this->hasMany(FailedDelivery::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the recipient's outbound messages.
|
||||
*/
|
||||
public function outboundMessages()
|
||||
{
|
||||
return $this->hasMany(OutboundMessage::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the user's custom domains.
|
||||
*/
|
||||
@ -114,6 +152,30 @@ class Recipient extends Model
|
||||
return $this->hasMany(Username::class, 'default_recipient_id', 'id');
|
||||
}
|
||||
|
||||
public function domainAliasesUsingAsDefault()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Alias::class,
|
||||
Domain::class,
|
||||
'default_recipient_id', // Foreign key on the domain table...
|
||||
'aliasable_id', // Foreign key on the alias table...
|
||||
'id', // Local key on the recipient table...
|
||||
'id' // Local key on the domain table...
|
||||
);
|
||||
}
|
||||
|
||||
public function usernameAliasesUsingAsDefault()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Alias::class,
|
||||
Username::class,
|
||||
'default_recipient_id', // Foreign key on the username table...
|
||||
'aliasable_id', // Foreign key on the alias table...
|
||||
'id', // Local key on the recipient table...
|
||||
'id' // Local key on the username table...
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the recipient has a verified email address.
|
||||
*
|
||||
|
||||
@ -8,8 +8,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Rule extends Model
|
||||
{
|
||||
use HasUuid;
|
||||
use HasFactory;
|
||||
use HasUuid;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\DisplayFromFormat;
|
||||
use App\Notifications\CustomResetPassword;
|
||||
use App\Notifications\CustomVerifyEmail;
|
||||
use App\Traits\HasEncryptedAttributes;
|
||||
use App\Traits\HasUuid;
|
||||
@ -17,11 +19,11 @@ use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
use Notifiable;
|
||||
use HasUuid;
|
||||
use HasEncryptedAttributes;
|
||||
use HasApiTokens;
|
||||
use HasEncryptedAttributes;
|
||||
use HasFactory;
|
||||
use HasUuid;
|
||||
use Notifiable;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
@ -37,8 +39,12 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'from_name',
|
||||
'email_subject',
|
||||
'banner_location',
|
||||
'display_from_format',
|
||||
'catch_all',
|
||||
'bandwidth',
|
||||
'reject_until',
|
||||
'defer_until',
|
||||
'defer_new_aliases_until',
|
||||
'default_alias_domain',
|
||||
'default_alias_format',
|
||||
'use_reply_to',
|
||||
@ -85,6 +91,10 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'email_verified_at' => 'datetime',
|
||||
'reject_until' => 'datetime',
|
||||
'defer_until' => 'datetime',
|
||||
'defer_new_aliases_until' => 'datetime',
|
||||
'display_from_format' => DisplayFromFormat::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@ -153,6 +163,26 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's default alias domain.
|
||||
*/
|
||||
protected function defaultAliasDomain(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (?string $value) => $value ?? 'anonaddy.me',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's default alias format.
|
||||
*/
|
||||
protected function defaultAliasFormat(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (?string $value) => $value ?? 'random_characters',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's default username.
|
||||
*/
|
||||
@ -177,6 +207,14 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return $this->hasMany(Alias::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the user's email aliases including trashed.
|
||||
*/
|
||||
public function allAliases()
|
||||
{
|
||||
return $this->hasMany(Alias::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the user's recipients.
|
||||
*/
|
||||
@ -185,6 +223,14 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return $this->hasMany(Recipient::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the user's pending recipients.
|
||||
*/
|
||||
public function pendingRecipients()
|
||||
{
|
||||
return $this->recipients()->pending();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the user's custom domains.
|
||||
*/
|
||||
@ -209,6 +255,14 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return $this->hasMany(FailedDelivery::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the user's outbound messages.
|
||||
*/
|
||||
public function outboundMessages()
|
||||
{
|
||||
return $this->hasMany(OutboundMessage::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the user's active rules.
|
||||
*/
|
||||
@ -321,6 +375,30 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count for the user's aliases that are using the default recipient
|
||||
*/
|
||||
public function aliasesUsingDefaultCount()
|
||||
{
|
||||
return $this->aliases()->select('id')->where(function (Builder $q) {
|
||||
return $q->whereHas('recipients', function (Builder $query) {
|
||||
$query->where('recipients.id', $this->default_recipient_id);
|
||||
})
|
||||
->orWhereDoesntHave('recipients')->where(function (Builder $q) {
|
||||
return $q->whereDoesntHaveMorph(
|
||||
'aliasable',
|
||||
['App\Models\Domain', 'App\Models\Username'],
|
||||
function (Builder $query) {
|
||||
$query->whereNotNull('default_recipient_id');
|
||||
}
|
||||
)->orWhereNull('aliasable_id')
|
||||
->orWhereHasMorph('aliasable', ['App\Models\Domain', 'App\Models\Username'], function (Builder $query) {
|
||||
$query->where('default_recipient_id', $this->default_recipient_id);
|
||||
});
|
||||
});
|
||||
})->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the email verification notification.
|
||||
*
|
||||
@ -331,6 +409,17 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
$this->notify(new CustomVerifyEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$this->notify(new CustomResetPassword($token));
|
||||
}
|
||||
|
||||
public function hasVerifiedDefaultRecipient()
|
||||
{
|
||||
return ! is_null($this->defaultRecipient->email_verified_at);
|
||||
@ -390,6 +479,11 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return false;
|
||||
},
|
||||
function () {
|
||||
$now = now();
|
||||
if ($this->defer_new_aliases_until < $now) {
|
||||
$this->update(['defer_new_aliases_until' => $now->addHour()->toDateTimeString()]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
@ -510,4 +604,9 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
->reverse()
|
||||
->values();
|
||||
}
|
||||
|
||||
public function sharedDomainOptions()
|
||||
{
|
||||
return config('anonaddy.all_domains');
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,9 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Username extends Model
|
||||
{
|
||||
use HasUuid;
|
||||
use HasEncryptedAttributes;
|
||||
use HasFactory;
|
||||
use HasUuid;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
@ -19,14 +19,17 @@ class Username extends Model
|
||||
|
||||
protected $encrypted = [
|
||||
'description',
|
||||
'from_name',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'username',
|
||||
'description',
|
||||
'from_name',
|
||||
'active',
|
||||
'catch_all',
|
||||
'can_login',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@ -34,6 +37,7 @@ class Username extends Model
|
||||
'user_id' => 'string',
|
||||
'active' => 'boolean',
|
||||
'catch_all' => 'boolean',
|
||||
'can_login' => 'boolean',
|
||||
'default_recipient_id' => 'string',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
@ -121,4 +125,20 @@ class Username extends Model
|
||||
{
|
||||
$this->update(['catch_all' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow login for the username.
|
||||
*/
|
||||
public function disallowLogin()
|
||||
{
|
||||
$this->update(['can_login' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow login for the username.
|
||||
*/
|
||||
public function allowLogin()
|
||||
{
|
||||
$this->update(['can_login' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class AliasesImportedNotification extends Notification implements ShouldQueue, ShouldBeEncrypted
|
||||
class AliasesImportedNotification extends Notification implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
|
||||
140
app/Notifications/CustomResetPassword.php
Normal file
140
app/Notifications/CustomResetPassword.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class CustomResetPassword extends Notification implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* The password reset token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* The callback that should be used to create the reset password URL.
|
||||
*
|
||||
* @var (\Closure(mixed, string): string)|null
|
||||
*/
|
||||
public static $createUrlCallback;
|
||||
|
||||
/**
|
||||
* The callback that should be used to build the mail message.
|
||||
*
|
||||
* @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage)|null
|
||||
*/
|
||||
public static $toMailCallback;
|
||||
|
||||
/**
|
||||
* Create a notification instance.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array|string
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
if (static::$toMailCallback) {
|
||||
return call_user_func(static::$toMailCallback, $notifiable, $this->token);
|
||||
}
|
||||
|
||||
return $this->buildMailMessage($this->resetUrl($notifiable), $notifiable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reset password notification mail message for the given URL.
|
||||
*
|
||||
* @param string $url
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
protected function buildMailMessage($url, $notifiable)
|
||||
{
|
||||
$recipient = $notifiable->defaultRecipient;
|
||||
$fingerprint = $recipient->should_encrypt ? $recipient->fingerprint : null;
|
||||
|
||||
return (new MailMessage)
|
||||
->subject(Lang::get('Reset Password Notification'))
|
||||
->markdown('mail.reset_password', [
|
||||
'resetUrl' => $url,
|
||||
'userId' => $notifiable->id,
|
||||
'recipientId' => $notifiable->default_recipient_id,
|
||||
'emailType' => 'RP',
|
||||
'fingerprint' => $fingerprint,
|
||||
])
|
||||
->withSymfonyMessage(function (Email $message) {
|
||||
$message->getHeaders()
|
||||
->addTextHeader('Feedback-ID', 'RP:anonaddy');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reset URL for the given notifiable.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return string
|
||||
*/
|
||||
protected function resetUrl($notifiable)
|
||||
{
|
||||
if (static::$createUrlCallback) {
|
||||
return call_user_func(static::$createUrlCallback, $notifiable, $this->token);
|
||||
}
|
||||
|
||||
return url(route('password.reset', [
|
||||
'token' => $this->token,
|
||||
'email' => $notifiable->getEmailForPasswordReset(),
|
||||
], false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback that should be used when creating the reset password button URL.
|
||||
*
|
||||
* @param \Closure(mixed, string): string $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function createUrlUsing($callback)
|
||||
{
|
||||
static::$createUrlCallback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback that should be used when building the notification mail message.
|
||||
*
|
||||
* @param \Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function toMailUsing($callback)
|
||||
{
|
||||
static::$toMailCallback = $callback;
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@ use Illuminate\Support\Facades\Lang;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class CustomVerifyEmail extends VerifyEmail implements ShouldQueue, ShouldBeEncrypted
|
||||
class CustomVerifyEmail extends VerifyEmail implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
@ -35,12 +35,15 @@ class CustomVerifyEmail extends VerifyEmail implements ShouldQueue, ShouldBeEncr
|
||||
|
||||
$feedbackId = $notifiable instanceof User ? 'VU:anonaddy' : 'VR:anonaddy';
|
||||
$recipientId = $notifiable instanceof User ? $notifiable->default_recipient_id : $notifiable->id;
|
||||
$userId = $notifiable instanceof User ? $notifiable->id : $notifiable->user_id;
|
||||
|
||||
return (new MailMessage())
|
||||
->subject(Lang::get('Verify Email Address'))
|
||||
->markdown('mail.verify_email', [
|
||||
'verificationUrl' => $verificationUrl,
|
||||
'userId' => $userId,
|
||||
'recipientId' => $recipientId,
|
||||
'emailType' => $feedbackId,
|
||||
])
|
||||
->withSymfonyMessage(function (Email $message) use ($feedbackId) {
|
||||
$message->getHeaders()
|
||||
|
||||
@ -9,7 +9,7 @@ use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class DefaultRecipientUpdated extends Notification implements ShouldQueue, ShouldBeEncrypted
|
||||
class DefaultRecipientUpdated extends Notification implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
@ -49,7 +49,9 @@ class DefaultRecipientUpdated extends Notification implements ShouldQueue, Shoul
|
||||
->markdown('mail.default_recipient_updated', [
|
||||
'defaultRecipient' => $notifiable->email,
|
||||
'newDefaultRecipient' => $this->newDefaultRecipient,
|
||||
'userId' => $notifiable->user_id,
|
||||
'recipientId' => $notifiable->id,
|
||||
'emailType' => 'DRU',
|
||||
'fingerprint' => $notifiable->should_encrypt ? $notifiable->fingerprint : null,
|
||||
])
|
||||
->withSymfonyMessage(function (Email $message) {
|
||||
|
||||
@ -10,7 +10,7 @@ use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class DisallowedReplySendAttempt extends Notification implements ShouldQueue, ShouldBeEncrypted
|
||||
class DisallowedReplySendAttempt extends Notification implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
@ -63,7 +63,9 @@ class DisallowedReplySendAttempt extends Notification implements ShouldQueue, Sh
|
||||
'recipient' => $this->recipient,
|
||||
'destination' => $this->destination,
|
||||
'authenticationResults' => $this->authenticationResults,
|
||||
'userId' => $notifiable->user_id,
|
||||
'recipientId' => $notifiable->id,
|
||||
'emailType' => 'DRSA',
|
||||
'fingerprint' => $fingerprint,
|
||||
])
|
||||
->withSymfonyMessage(function (Email $message) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user