소스 검색

Added catch-all option to main account username

Will Browning 4 년 전
부모
커밋
f00b282976

+ 3 - 3
.env.example

@@ -43,7 +43,7 @@ ANONADDY_LIMIT=200
 ANONADDY_BANDWIDTH_LIMIT=104857600
 ANONADDY_NEW_ALIAS_LIMIT=10
 ANONADDY_ADDITIONAL_USERNAME_LIMIT=3
-ANONADDY_SIGNING_KEY_FINGERPRINT=your-signing-key-fingerprint
-# This is only needed if you will be adding any custom domains. If you do not need it then leave it blank e.g. ANONADDY_DKIM_SIGNING_KEY=
-ANONADDY_DKIM_SIGNING_KEY=/path/to/your/private/dkim/signing/key
+ANONADDY_SIGNING_KEY_FINGERPRINT=
+# This is only needed if you will be adding any custom domains. If you do not need it then leave it blank. ANONADDY_DKIM_SIGNING_KEY=/etc/opendkim/keys/example.com/default.private
+ANONADDY_DKIM_SIGNING_KEY=
 ANONADDY_DKIM_SELECTOR=default

+ 1 - 0
README.md

@@ -191,6 +191,7 @@ A few days before your billing cycle ends you will receive an email letting you
 * 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 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.
 

+ 239 - 92
SELF-HOSTING.md

@@ -6,19 +6,19 @@ Choosing a provider (that you trust), Vultr, Greenhost, OVH, Hetzner, Linode, Co
 
 With Vultr you may need to open a support ticket and request for them to unblock port 25.
 
-Before starting you will want to check the IP of your new server to make sure it is not on any blacklists - https://multirbl.valli.org/lookup/
+Before starting you will want to check the IP of your new server to make sure it is not on any blacklists - [https://multirbl.valli.org/lookup/](https://multirbl.valli.org/lookup/)
 
 If it is, destroy it and deploy a new one. You might notice that some providers such as Vultr have entire ranges of IPs listed.
 
-You should have a fresh 18.04 Ubuntu server. I'm assuming that you have taken proper steps to secure the server (no root login, key auth only, 2FA, automatic security updates etc.).
+You should have a fresh 20.04 Ubuntu server (or 18.04). I'm assuming that you have taken proper steps to secure the server (no root login, key auth only, 2FA, automatic security updates etc.).
 
 Add Fail2ban, a Firewall (e.g UFW), make sure that ports 25, 22 (or whatever your SSH port is if you've changed it) 443 and 80 are open.
 
-A good place to get started - https://github.com/imthenachoman/How-To-Secure-A-Linux-Server
+A good place to get started - [https://github.com/imthenachoman/How-To-Secure-A-Linux-Server](https://github.com/imthenachoman/How-To-Secure-A-Linux-Server)
 
-https://jacyhong.wordpress.com/2016/06/27/my-first-10-minutes-on-a-server-primer-for-securing-ubuntu/
+[https://jacyhong.wordpress.com/2016/06/27/my-first-10-minutes-on-a-server-primer-for-securing-ubuntu/](https://jacyhong.wordpress.com/2016/06/27/my-first-10-minutes-on-a-server-primer-for-securing-ubuntu/)
 
-https://plusbryan.com/my-first-5-minutes-on-a-server-or-essential-security-for-linux-servers
+[https://plusbryan.com/my-first-5-minutes-on-a-server-or-essential-security-for-linux-servers](https://plusbryan.com/my-first-5-minutes-on-a-server-or-essential-security-for-linux-servers)
 
 I will be running all commands as a sudo user called `johndoe`. The domain used will be `example.com` and the hostname `mail.example.com`. I'll be using Vultr for this example (Note: if you also use Vultr for managing DNS records they do not currently support TSLA records required for DANE).
 
@@ -48,7 +48,7 @@ MX @ mail.example.com
 
 We want to direct it to our server's fully qualifed domain name (FQDN). Give it a priority of 10 (or just make sure it has the lowest priority if you have other MX records).
 
-If you want to use wildcard subdomains e.g. (alias@username.example.com) then you also need to add a wildcard MX record:
+If you want to be able to also use wildcard subdomains e.g. (alias@username.example.com) then you also need to add a wildcard MX record:
 
 ```
 MX * mail.example.com
@@ -63,7 +63,7 @@ A * <Your-IPv4-address>
 AAAA * <Your-IPv4-address>
 ```
 
-If you want to just use the example.com domain and not bother with subdomains then you can skip the wildcard MX, A, AAAA records above (you will still need to add one for unsubscribe.example.com though to handle deactivating aliases).
+If you want to just use the example.com domain and not bother with subdomains then you can skip the wildcard MX, A, AAAA records above (you will still need to add MX and A/AAAA for unsubscribe.example.com though to handle deactivating aliases).
 
 Next we will add an explicit A record for the hostname `mail.example.com` and for where the web app will be located `app.example.com`
 
@@ -91,6 +91,8 @@ In Vultr you can update your reverse DNS by clicking on your server, then going
 
 Change it to `mail.example.com`. Don't forget to update this for IPv6 if you are using it too.
 
+You can check that it is set correctly by entering your IPv4 and IPv6 addresses here [https://mxtoolbox.com/ReverseLookup.aspx](https://mxtoolbox.com/ReverseLookup.aspx).
+
 ## Installing Postfix
 
 Now we're going to install our MTA (mail transfer agent) Postfix.
@@ -111,7 +113,7 @@ If you would like to check the version of Postfix that you are running you can d
 sudo postconf mail_version
 ```
 
-At the time of writing this I am running `mail_version = 3.3.0`.
+At the time of writing this I am running `mail_version = 3.4.13`.
 
 We'll install an extension we will need later so that Postfix can query our database.
 
@@ -211,10 +213,12 @@ smtpd_sender_restrictions =
    reject_unknown_sender_domain
    reject_unknown_reverse_client_hostname
 
+policyd-spf_time_limit = 3600
+
 smtpd_recipient_restrictions =
    permit_mynetworks,
    reject_unauth_destination,
-   check_recipient_access mysql:/etc/postfix/mysql-recipient-access.cf, mysql:/etc/postfix/mysql-recipient-access-domains-and-additional-usernames.cf,
+   check_recipient_access mysql:/etc/postfix/mysql-recipient-access.cf,
    check_policy_service unix:private/policyd-spf
    reject_rhsbl_helo dbl.spamhaus.org,
    reject_rhsbl_reverse_client dbl.spamhaus.org,
@@ -241,7 +245,7 @@ Make sure your hostname is correct in the Postfix config file.
 sudo postconf myhostname
 ```
 
-You should see mail.example.com if you don't edit `/etc/postfix/main.cf` and update the myhostname value.
+You'll see warnings that the mysql-... files do not exist. You should see mail.example.com if you don't edit `/etc/postfix/main.cf` and update the myhostname value.
 
 Open up `/etc/postfix/master.cf` and update this line at the top of the file:
 
@@ -265,7 +269,15 @@ This command will pipe the email through to our applicaton so that we can determ
 
 ## Installing Nginx
 
-We'll install the mainline version of Nginx.
+On Ubuntu 20.04 Nginx is included in the default repositories so we can simply run:
+
+```bash
+sudo apt update
+sudo apt install nginx
+sudo nginx -v
+```
+
+If you're on Ubuntu 18.04 you will need to add the following signing key and repo.
 
 Import the nginx signing key and the repository.
 
@@ -274,7 +286,7 @@ sudo apt-key adv --fetch-keys 'https://nginx.org/keys/nginx_signing.key'
 sudo sh -c "echo 'deb https://nginx.org/packages/mainline/ubuntu/ '$(lsb_release -cs)' nginx' > /etc/apt/sources.list.d/Nginx.list"
 ```
 
-Install and check the version.
+Then you can Install and check the version.
 
 ```bash
 sudo apt update
@@ -282,7 +294,7 @@ sudo apt install nginx
 sudo nginx -v
 ```
 
-At the time of writing this I have `nginx version: nginx/1.17.8`.
+At the time of writing this I have `nginx version: nginx/1.18.0`.
 
 Create the directory for where the application will be stored.
 
@@ -299,7 +311,7 @@ sudo mkdir /etc/nginx/ssl
 sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096
 ```
 
-The above command wil take quite some time.
+The above command will take quite some time, so go grab a cup of tea/coffee!
 
 Next create the Nginx server block:
 
@@ -386,6 +398,8 @@ We won't restart nginx yet because it won't be able to find the SSL certificates
 
 We're going to install the latest version of PHP at the time of writing this - version 7.4
 
+If you are using Ubuntu 18.04 you will need to add the below repository, Ubuntu 20.04 can skip this step.
+
 ```bash
 sudo apt install software-properties-common
 sudo add-apt-repository ppa:ondrej/php
@@ -434,11 +448,12 @@ git clone https://github.com/acmesh-official/acme.sh.git
 cd ./acme.sh
 ./acme.sh --install
 ```
-You should set up automatic DNS API integration for wildcard certs if you are using them, this will allow automatic renewal of certificates.
 
-https://github.com/acmesh-official/acme.sh#8-automatic-dns-api-integration
+You should set up automatic DNS API integration for wildcard certs if you are using them, this will allow automatic renewal of your certificates.
 
-For example instructions for Vultr are here - https://github.com/acmesh-official/acme.sh/wiki/dnsapi#82-use-vultr-dns-api-to-automatically-issue-cert
+[https://github.com/acmesh-official/acme.sh#8-automatic-dns-api-integration](https://github.com/acmesh-official/acme.sh#8-automatic-dns-api-integration)
+
+For example, instructions for Vultr are here - [https://github.com/acmesh-official/acme.sh/wiki/dnsapi#82-use-vultr-dns-api-to-automatically-issue-cert](https://github.com/acmesh-official/acme.sh/wiki/dnsapi#82-use-vultr-dns-api-to-automatically-issue-cert)
 
 I would run:
 
@@ -458,6 +473,8 @@ To install the certificate run:
 
 Make sure to change example.com to your domain.
 
+You can now type `exit` to go back to the `johndoe` user instead of `root`.
+
 ## SPF and DKIM
 
 Follow the instructions in the linked blog post at the end of this section on how to install OpenDKIM and then add an SPF record.
@@ -476,7 +493,7 @@ Also when editing `/etc/opendkim/signing.table` add this line too so that emails
 *@*.example.com    default._domainkey.example.com
 ```
 
-https://www.linuxbabe.com/mail-server/setting-up-dkim-and-spf
+[https://www.linuxbabe.com/mail-server/setting-up-dkim-and-spf](https://www.linuxbabe.com/mail-server/setting-up-dkim-and-spf)
 
 Once you've finished following the above post you should have SPF and DKIM set up for your domain.
 
@@ -484,21 +501,25 @@ Once you've finished following the above post you should have SPF and DKIM set u
 
 Next follow this blog post on how to install OpenDMARC.
 
-https://www.linuxbabe.com/mail-server/opendmarc-postfix-ubuntu
+[https://www.linuxbabe.com/mail-server/opendmarc-postfix-ubuntu](https://www.linuxbabe.com/mail-server/opendmarc-postfix-ubuntu)
+
+Next add a new TXT record to your domain for DMARC with a host of `@` and value:
 
-Then take a look at this one on how to add a DMARC record to your domain:
+```
+v=DMARC1; p=none; pct=100; rua=mailto:dmarc-reports@example.com
+```
 
-https://www.linuxbabe.com/mail-server/create-dmarc-record
+For further reading about DMARC records and the different options available see - [https://www.linuxbabe.com/mail-server/create-dmarc-record](https://www.linuxbabe.com/mail-server/create-dmarc-record)
 
-After following those posts you should now have a valid DMARC record for your domain.
+You should now have a valid DMARC record for your domain.
 
 ## Installing MariaDB
 
-At the time of writing this the latest stable release is v10.4. Make sure to check for any newer releases.
+At the time of writing this the latest stable release is v10.5. Make sure to check for any newer releases.
 
-Follow the instructions on this link to install MariaDB:
+Follow the instructions on this link to install MariaDB (make sure to change to 18.04 if you are using it):
 
-https://downloads.mariadb.org/mariadb/repositories/#distro=Ubuntu&distro_release=bionic--ubuntu_bionic&mirror=digitalocean-sfo&version=10.4
+[https://downloads.mariadb.org/mariadb/repositories/#distro=Ubuntu&distro_release=focal--ubuntu_focal&mirror=digital-pacific&version=10.5](https://downloads.mariadb.org/mariadb/repositories/#distro=Ubuntu&distro_release=focal--ubuntu_focal&mirror=digital-pacific&version=10.5)
 
 Make sure it is running correctly and check the version
 
@@ -507,9 +528,11 @@ sudo systemctl status mariadb
 sudo mysql -V
 ```
 
-At the time of writing this I am using "Ver 15.1 Distrib 10.4.12-MariaDB"
+At the time of writing this I am using "Ver 15.1 Distrib 10.5.6-MariaDB"
 
-Keep the default answers when running the below but set a secure MySQL root password and make a note of it somewhere e.g. password manager.
+Set a secure MySQL root password by running the command below and make a note of it somewhere e.g. password manager.
+
+Answer `no` for "Switch to unix_socket authentication" and `no` for "Change the root password?" as you have already set it in the first step. Answer `yes` (default) to the other questions.
 
 ```bash
 sudo mysql_secure_installation
@@ -538,7 +561,7 @@ Grant the user privileges for the new database.
 GRANT ALL PRIVILEGES ON anonaddy_database.* TO 'anonaddy'@'localhost' WITH GRANT OPTION;
 ```
 
-Flush privileges.
+Next flush privileges and exit the MariaDB shell.
 
 ```sql
 FLUSH PRIVILEGES;
@@ -563,91 +586,172 @@ query = SELECT (SELECT 1 FROM users WHERE '%s' IN (CONCAT(username, '.example.co
 
 This file is responsible for determining whether the server should accept email for a certain domain/subdomain. If no results are found from the query then the email will not be accepted.
 
-Next create another new file `/etc/postfix/mysql-recipient-access-domains-and-additional-usernames.cf` and enter the following inside:
+The reason these SQL queries are not all nicely formatted is because they have to be on one line.
+
+Next create another new file `/etc/postfix/mysql-recipient-access.cf` and enter the following inside:
 
 ```sql
 user = anonaddy
 password = your-database-password
 hosts = 127.0.0.1
 dbname = anonaddy_database
-query = SELECT (SELECT CASE WHEN NOT EXISTS(SELECT NULL FROM aliases WHERE email = '%s') AND additional_usernames.catch_all = 0 OR domains.catch_all = 0 THEN "REJECT" WHEN additional_usernames.active = 0 OR domains.active = 0 THEN "DISCARD" ELSE NULL END FROM additional_usernames, domains WHERE SUBSTRING_INDEX('%s', '@',-1) IN (CONCAT(additional_usernames.username, '.example.com')) OR domains.domain = SUBSTRING_INDEX('%s', '@',-1) LIMIT 1) AS result LIMIT 1;
+query = CALL check_access('%s')
 ```
 
-If you need to add multiple domains then just update the IN section to:
+This file is responsible for checking first whether an alias exists, and if so has it been deactivated or deleted. If it has been deactivated or deleted then return 'DISCARD' or 'REJECT'.
 
-```sql
-IN (CONCAT(additional_usernames.username, '.example.com'),CONCAT(additional_usernames.username, '.example2.com'))
-```
+If the alias has not been deactivated or deleted or it does not exist then it also checks whether the alias is for a user, additional username or custom domain and if so, is that additional username or custom domain set as active. If it is not set as active then the email is discarded. It also checks if the user, additional usename or custom domain has catch-all enabled, and if not and the alias does not already exist then the email is rejected.
 
-etc.
+The reason we're using a stored procedure here is so that we can run multiple queries and use IF statements.
 
-This file is responsible for checking whether the alias is for an additional username/custom domain and if so then is that additional username/custom domain set as active. If it is not set as active then the email is discarded. It also checks if the additional usename/custom domain has catch-all enabled and if not it checks if that alias already exists. If it does not already exist then the email is rejected.
+Either from the command line (`sudo mysql -u root -p`) or from an SQL client, run the following code to create the stored procedure.
 
-The reason these SQL queries are not all nicely formatted is because they have to be on one line.
+If you have any issues creating the stored procedure, make sure you have set appropriate permissions for your database user.
 
-Now we need to create a stored procedure that can be called.
+```sql
+DELIMITER $$
 
-In order for Postfix to REJECT or DISCARD emails sent to deleted or deactivated aliases you need to ceate a new file called `/etc/postfix/mysql-recipient-access.cf`.
+USE `anonaddy_database`$$
 
-In this file enter the following:
+DROP PROCEDURE IF EXISTS `check_access`$$
 
-```
-user = anonaddy
-password = your-database-password
-hosts = 127.0.0.1
-dbname = anonaddy_database
-query = CALL block_alias('%s')
-```
+CREATE DEFINER=`anonaddy`@`localhost` PROCEDURE `check_access`(alias_email VARCHAR(254) charset utf8)
+BEGIN
+    DECLARE alias_action varchar(7) charset utf8;
+    DECLARE no_alias_exists int(1);
+    DECLARE alias_domain varchar(254) charset utf8;
+    SET alias_domain = SUBSTRING_INDEX(alias_email, '@', -1);
+
+    # We only want to carry out the checks if it is a full RCPT TO address without any + extension
+    IF LOCATE('@',alias_email) > 1 AND LOCATE('+',alias_email) = 0 AND LENGTH(alias_domain) > 0 THEN
+
+        SET no_alias_exists = CASE WHEN NOT EXISTS(SELECT NULL FROM aliases WHERE email = alias_email) THEN 1 ELSE 0 END;
+
+        # If there is an alias, check if it is deactivated or deleted
+        IF NOT no_alias_exists THEN
+            SET alias_action = (SELECT
+                IF(deleted_at IS NULL,
+                'DISCARD',
+                'REJECT')
+            FROM
+                aliases
+            WHERE
+                email = alias_email
+                AND (active = 0
+                OR deleted_at IS NOT NULL) LIMIT 1);
+        END IF;
+
+        # If the alias is deactivated or deleted then increment its blocked count and return the alias_action
+        IF alias_action IN('DISCARD','REJECT') THEN
+            UPDATE
+                aliases
+            SET
+                emails_blocked = emails_blocked + 1
+            WHERE
+                email = alias_email
+            LIMIT 1;
+
+            SELECT alias_action;
+        ELSE
+            SELECT
+            (
+            SELECT
+                CASE
+                    WHEN no_alias_exists
+                    AND catch_all = 0 THEN "REJECT"
+                    ELSE NULL
+                END
+            FROM
+                users
+            WHERE
+                alias_domain IN ( CONCAT(username, '.example.com')) ) AS users,
+            (
+            SELECT
+                CASE
+                    WHEN no_alias_exists
+                    AND catch_all = 0 THEN "REJECT"
+                    WHEN active = 0 THEN "DISCARD"
+                    ELSE NULL
+                END
+            FROM
+                additional_usernames
+            WHERE
+                alias_domain IN ( CONCAT(username, '.example.com')) ) AS usernames,
+            (
+            SELECT
+                CASE
+                    WHEN no_alias_exists
+                    AND catch_all = 0 THEN "REJECT"
+                    WHEN active = 0 THEN "DISCARD"
+                    ELSE NULL
+                END
+            FROM
+                domains
+            WHERE
+                domain = alias_domain) AS domains
+            LIMIT 1;
+        END IF;
+    ELSE
+        SELECT NULL;
+    END IF;
+ END$$
 
-This query calls a stored procedure that we will create next, it passes the recipient's email address as the argument and checks to see if the alias is either deactivated or has previously been deleted and returns the appropriate response (DISCARD for deactivated and REJECT for deleted).
+DELIMITER ;
+```
 
-The reason we're using a stored procedure here is because we need to run more than one SQL query which means we cannot just add it inline to `/etc/postfix/mysql-recipient-access.cf` as we have with the others.
+If you need to add multiple domains then just update both of the IN sections to:
 
-Update the permissions and the group of all these files:
+```sql
+IN (CONCAT(username, '.example.com'),CONCAT(username, '.example2.com'))
+```
 
-```bash
-sudo chmod o= /etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf /etc/postfix/mysql-recipient-access-domains-and-additional-usernames.cf /etc/postfix/mysql-recipient-access.cf
+You may be wondering why we have this line near the top of the procedure:
 
-sudo chgrp postfix /etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf /etc/postfix/mysql-recipient-access-domains-and-additional-usernames.cf /etc/postfix/mysql-recipient-access.cf
+```sql
+IF LOCATE('@',alias_email) > 1 AND LOCATE('+',alias_email) = 0 AND LENGTH(alias_domain) > 0 THEN
 ```
 
-Either from the command line (`sudo mysql -u root -p`) or from an SQL client, run the following code to create the stored procedure.
+The reason this is present is because Postfix will pass multiple arguments to this stored procedure for each incoming email.
 
-You will need to set appropriate permissions for your database user to allow them to execute the stored procedure.
+From the Postfix docs for [check_recipient_access](http://www.postfix.org/postconf.5.html#check_recipient_access):
+
+> "Search the specified access(5) database for the resolved RCPT TO address, domain, parent domains, or localpart@, and execute the corresponding action."
+
+What this means is that if an email comes in for the alias - hello+extension@username.example.com then Postfix will run the stored procedure with the following arguements:
 
 ```sql
-DELIMITER $$
+CALL check_access('hello+extension@username.example.com');
+CALL check_access('hello@username.example.com');
+CALL check_access('username.example.com');
+CALL check_access('example.com');
+CALL check_access('com');
+CALL check_access('hello@');
+```
 
-USE `anonaddy_database`$$
+We only want the queries to be run for the RCPT TO address (hello@username.example.com) without any + extension, which is what the check above does. It also prevents needless database queries being run.
 
-DROP PROCEDURE IF EXISTS `block_alias`$$
+Update the permissions and the group of these files:
 
-CREATE DEFINER=`anonaddy`@`localhost` PROCEDURE `block_alias`(alias_email VARCHAR(254))
-BEGIN
-   UPDATE aliases SET
-    emails_blocked = emails_blocked + 1
-   WHERE email = alias_email AND active = 0 LIMIT 1;
-   SELECT IF(deleted_at IS NULL,'DISCARD','REJECT') AS alias_action
-   FROM aliases WHERE email = alias_email AND (active = 0 OR deleted_at IS NOT NULL) LIMIT 1;
- END$$
+```bash
+sudo chmod o= /etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf /etc/postfix/mysql-recipient-access.cf
 
-DELIMITER ;
+sudo chgrp postfix /etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf /etc/postfix/mysql-recipient-access.cf
 ```
 
 Make a test call for the stored procedure as your database user to ensure everything is working as expected.
 
 ```sql
 USE anonaddy_database;
-CALL block_alias('email@example.com');
+CALL check_access('email@example.com');
 ```
 
 You will get an error stating "Table 'anonaddy_database.aliases' doesn't exist" as we have not yet migrated the database.
 
 ## Installing Redis
 
-Follow this blog post on Digital Ocean to install Redis.
+Follow this short post on Digital Ocean to install Redis.
 
-https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-18-04
+[https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-20-04](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-20-04)
 
 We'll be using Redis for queues, user limits, sessions and caching.
 
@@ -661,21 +765,29 @@ git clone https://github.com/anonaddy/anonaddy.git
 cd /var/www/anonaddy
 ```
 
-Make sure composer is installed (`composer -V`), if not then goto - https://getcomposer.org/download/ for instructions.
+Make sure composer is installed (`composer -V`), if not then goto - [https://getcomposer.org/download/](https://getcomposer.org/download/) for instructions.
 
-You can add the following flags when installing composer:
+You can add the following flags when running the composer-setup.php command to add it to your $PATH:
 
 ```bash
 sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
 ```
 
-Make sure node is installed (`node -v`) if not then install it using NVM - https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-18-04#installing-using-nvm
+Before running the NVM install script below make sure that you have a `~/.bashrc` file. If not create one by running `touch ~/.bashrc` so that the NVM installer can be added to your $PATH. Also create a `~/.bash_profile` and add:
+
+```bash
+if [ -f ~/.bashrc ]; then
+  . ~/.bashrc
+fi
+```
+
+Make sure node is installed (`node -v`) if not then install it using NVM - [https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04#option-3-%E2%80%94-installing-node-using-the-node-version-manager](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04#option-3-%E2%80%94-installing-node-using-the-node-version-manager)
 
-At the time of writing this I'm using the latest LTS - ........
+At the time of writing this I'm using the latest LTS - v12.19.0
 
 ```bash
 cd /var/www/anonaddy
-composer install && npm install
+composer install --prefer-dist --no-scripts --no-dev -o && npm install
 npm run production
 ```
 
@@ -686,15 +798,17 @@ cp .env.example .env
 nano .env
 ```
 
-Make sure to update the database settings and the AnonAddy variables, you can use Redis for queue, sessions and cache.
+Make sure to update the database settings, redis password and the AnonAddy variables. You can use Redis for queue, sessions and cache.
+
+We'll set `ANONADDY_SIGNING_KEY_FINGERPRINT` shortly.
 
 `APP_KEY` will be generted in the next step, this is used by Laravel for securely encrypting values.
 
-For more information on Laravel configuration please visit - https://laravel.com/docs/8.x/installation#configuration
+For more information on Laravel configuration please visit - [https://laravel.com/docs/8.x/installation#configuration](https://laravel.com/docs/8.x/installation#configuration)
 
 For the `ANONADDY_DKIM_SIGNING_KEY` you only need to fill in this variable if you plan to add any custom domains through the web application.
 
-You can either use the same private DKIM signing key we generated earlier from this tutorial - https://www.linuxbabe.com/mail-server/setting-up-dkim-and-spf
+You can either use the same private DKIM signing key we generated earlier from this tutorial - [https://www.linuxbabe.com/mail-server/setting-up-dkim-and-spf](https://www.linuxbabe.com/mail-server/setting-up-dkim-and-spf)
 
 Or you can generate a new private/public keypair and give your user `johndoe` ownership of the private key.
 
@@ -704,9 +818,25 @@ If you want to use the same key we already generated then you will need to add `
 sudo usermod -a -G opendkim johndoe
 ```
 
-Make sure to also run `sudo chmod g+r /path/to/dkim/private/key` so that your johndoe user has read permissions for the file.
+Make sure to also run `sudo chmod g+r /etc/opendkim/keys/example.com/default.private` so that your johndoe user has read permissions for the file.
+
+You'll need to log out and back in again for the changes to take effect.
+
+You can test it by running `cat /etc/opendkim/keys/example.com/default.private` as the johndoe user to see if it can be displayed.
+
+Doing this will cause opendkim to show a warning in your `mail.log` like this:
+
+```
+default._domainkey.example.com: key data is not secure: /etc/opendkim/keys/example.com/default.private is in group <group-number> which has multiple users
+```
 
-You can test it by running `cat /path/to/dkim/private/key` as the johndoe user to see if it can be displayed.
+If you'd like to suppress this warning then you can add `RequireSafeKeys false` to your `/etc/opendkim.conf` file and restart opendkim - `sudo service opendkim restart`.
+
+Then update your `.env` file.
+
+```
+ANONADDY_DKIM_SIGNING_KEY=/etc/opendkim/keys/example.com/default.private
+```
 
 Then we will generate an app key, migrate the database, link the storage directory, restart the queue and install laravel passport.
 
@@ -720,7 +850,7 @@ php artisan view:cache
 php artisan route:cache
 php artisan queue:restart
 
-php artisan passport:install --uuids
+php artisan passport:install
 ```
 
 ## Installing Supervisor
@@ -737,7 +867,7 @@ Create a new configuration file:
 sudo nano /etc/supervisor/conf.d/anonaddy.conf
 ```
 
-Enter the following inside (change user and command location if you need to):
+Enter the following inside (change user, command location and the number of processes if you need to):
 
 ```
 [program:anonaddy]
@@ -775,9 +905,21 @@ Just update the value of `ANONADDY_ENABLE_REGISTRATION` to false in your .env fi
 
 If you are using encryption and want to sign your forwarded emails then you'll need to create a new GPG key pair.
 
-A simple guide can be found here - https://www.linuxbabe.com/security/a-practical-guide-to-gpg-part-1-generate-your-keypair
+A simple guide can be found here - [https://www.linuxbabe.com/security/a-practical-guide-to-gpg-part-1-generate-your-keypair](https://www.linuxbabe.com/security/a-practical-guide-to-gpg-part-1-generate-your-keypair)
+
+You will need to generate a key pair without giving it a password because php-gnupg is not able to use keys that are password protected.
+
+To find your key's fingerprint run:
+
+```bash
+gpg -k
+```
+
+The fingerprint is 40 characters long and looks like this `26A987650243B28802524E2F809FD0D502E2F695`.
 
-Then update the value of `ANONADDY_SIGNING_KEY_FINGERPRINT=` in your .env file to match the fingerprint of your key e.g. `26A987650243B28802524E2F809FD0D502E2F695`.
+Then update the value of `ANONADDY_SIGNING_KEY_FINGERPRINT=` in your .env file to match the fingerprint of your key.
+
+Then run `php artisan config:cache` to update.
 
 ## What to do next
 
@@ -833,7 +975,7 @@ sudo systemctl restart postfix spamass-milter
 
 If you want to test if Spamassasin is working then send an email with the content from this link in it.
 
-https://spamassassin.apache.org/gtube/
+[https://spamassassin.apache.org/gtube/](https://spamassassin.apache.org/gtube/)
 
 It should be rejected with the message `ERROR_CODE :550, ERROR_CODE :5.7.1 Blocked by SpamAssassin`.
 
@@ -843,7 +985,11 @@ This is to speed up queries and to prevent you getting rate limited when queryin
 
 Follow the below blog post on how to install bind9.
 
-https://www.linuxbabe.com/ubuntu/set-up-local-dns-resolver-ubuntu-18-04-16-04-bind9
+[https://www.linuxbabe.com/ubuntu/set-up-local-dns-resolver-ubuntu-20-04-bind9](https://www.linuxbabe.com/ubuntu/set-up-local-dns-resolver-ubuntu-20-04-bind9)
+
+Or if you're using Ubuntu 18.04 then:
+
+[https://www.linuxbabe.com/ubuntu/set-up-local-dns-resolver-ubuntu-18-04-16-04-bind9](https://www.linuxbabe.com/ubuntu/set-up-local-dns-resolver-ubuntu-18-04-16-04-bind9)
 
 Now open up `/etc/nginx/conf.d/example.com.conf` and add these two lines below the ssl parameters.
 
@@ -880,6 +1026,11 @@ Update `/etc/spamassassin/local.cf` and add this near the top:
 dns_available yes
 ```
 
+Then restart spamassassin.
+
+```bash
+sudo service spamassassin restart
+```
 
 ## Updating
 
@@ -902,10 +1053,6 @@ php artisan queue:restart
 
 This should pull in any updates from the GitHub repository and update your dependencies. It will then run any migrations before finally clearing the cache and restarting the queue workers.
 
-## Adding DNSSEC
-
-
-
-// TODO
-DNSSEC, DANE (TLSA DNS record), SMTP TLS Reporting (TLS-RPT).
+## Credits
 
+A big thank you to Xiao Guoan over at [linuxbabe.com](https://www.linuxbabe.com/) for all of his amazing articles. I highly recommend you subscribe to his newsletter.

+ 19 - 0
app/Http/Controllers/Api/AppVersionController.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use PragmaRX\Version\Package\Facade as Version;
+
+class AppVersionController extends Controller
+{
+    public function index()
+    {
+        return response()->json([
+            'version' => Version::version(),
+            'major' => (int) Version::major(),
+            'minor' => (int) Version::minor(),
+            'patch' => (int) Version::patch()
+        ]);
+    }
+}

+ 19 - 0
app/Http/Controllers/CatchAllController.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\UpdateCatchAllRequest;
+
+class CatchAllController extends Controller
+{
+    public function update(UpdateCatchAllRequest $request)
+    {
+        if ($request->catch_all) {
+            user()->enableCatchAll();
+        } else {
+            user()->disableCatchAll();
+        }
+
+        return back()->with(['status' => $request->catch_all ? 'Catch-All Enabled Successfully' : 'Catch-All Disabled Successfully']);
+    }
+}

+ 3 - 3
app/Http/Controllers/ShowAliasController.php

@@ -10,9 +10,9 @@ class ShowAliasController extends Controller
             ->aliases()
             ->withTrashed()
             ->toBase()
-            ->selectRaw("sum(emails_forwarded) as forwarded")
-            ->selectRaw("sum(emails_blocked) as blocked")
-            ->selectRaw("sum(emails_replied) as replies")
+            ->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();
 
         return view('aliases.index', [

+ 30 - 0
app/Http/Requests/UpdateCatchAllRequest.php

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

+ 18 - 0
app/Models/User.php

@@ -31,6 +31,7 @@ class User extends Authenticatable implements MustVerifyEmail
         'from_name',
         'email_subject',
         'banner_location',
+        'catch_all',
         'bandwidth',
         'default_recipient_id',
         'password',
@@ -65,6 +66,7 @@ class User extends Authenticatable implements MustVerifyEmail
     protected $casts = [
         'id' => 'string',
         'default_recipient_id' => 'string',
+        'catch_all' => 'boolean',
         'two_factor_enabled' => 'boolean'
     ];
 
@@ -227,6 +229,22 @@ class User extends Authenticatable implements MustVerifyEmail
         });
     }
 
+    /**
+     * Disable catch-all for the user.
+     */
+    public function disableCatchAll()
+    {
+        $this->update(['catch_all' => false]);
+    }
+
+    /**
+     * Enable catch-all for the user.
+     */
+    public function enableCatchAll()
+    {
+        $this->update(['catch_all' => true]);
+    }
+
     public function hasVerifiedDefaultRecipient()
     {
         return ! is_null($this->defaultRecipient->email_verified_at);

+ 3 - 1
composer.json

@@ -22,6 +22,7 @@
         "mews/captcha": "^3.0.0",
         "php-mime-mail-parser/php-mime-mail-parser": "^6.0",
         "pragmarx/google2fa-laravel": "^1.3",
+        "pragmarx/version": "^1.2",
         "predis/predis": "^1.1",
         "ramsey/uuid": "^4.0"
     },
@@ -61,7 +62,8 @@
     "scripts": {
         "post-autoload-dump": [
             "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
-            "@php artisan package:discover --ansi"
+            "@php artisan package:discover --ansi",
+            "@php artisan version:absorb"
         ],
         "post-root-package-install": [
             "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

+ 334 - 131
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "ad58bfc75b60b69a29ad24bb107f9c9e",
+    "content-hash": "7cb9186d04129ba7823bd48a197b9db2",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -761,27 +761,27 @@
         },
         {
             "name": "dragonmantank/cron-expression",
-            "version": "3.0.1",
+            "version": "v3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/dragonmantank/cron-expression.git",
-                "reference": "fa4e95ff5a7f1d62c3fbc05c32729b7f3ca14b52"
+                "reference": "48212cdc0a79051d50d7fc2f0645c5a321caf926"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/fa4e95ff5a7f1d62c3fbc05c32729b7f3ca14b52",
-                "reference": "fa4e95ff5a7f1d62c3fbc05c32729b7f3ca14b52",
+                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/48212cdc0a79051d50d7fc2f0645c5a321caf926",
+                "reference": "48212cdc0a79051d50d7fc2f0645c5a321caf926",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1"
+                "php": "^7.1|^8.0"
             },
             "replace": {
                 "mtdowling/cron-expression": "^1.0"
             },
             "require-dev": {
-                "phpstan/phpstan": "^0.11",
-                "phpunit/phpunit": "^6.4|^7.0"
+                "phpstan/phpstan": "^0.11|^0.12",
+                "phpunit/phpunit": "^7.0|^8.0|^9.0"
             },
             "type": "library",
             "autoload": {
@@ -811,7 +811,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-08-21T02:30:13+00:00"
+            "time": "2020-10-13T01:26:01+00:00"
         },
         {
             "name": "egulias/email-validator",
@@ -1112,23 +1112,23 @@
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "7.1.1",
+            "version": "7.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "7427d6f99df41cc01f33cd59832f721c150ffdf3"
+                "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7427d6f99df41cc01f33cd59832f721c150ffdf3",
-                "reference": "7427d6f99df41cc01f33cd59832f721c150ffdf3",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79",
+                "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "guzzlehttp/promises": "^1.0",
-                "guzzlehttp/psr7": "^1.6.1",
-                "php": "^7.2.5",
+                "guzzlehttp/promises": "^1.4",
+                "guzzlehttp/psr7": "^1.7",
+                "php": "^7.2.5 || ^8.0",
                 "psr/http-client": "^1.0"
             },
             "provide": {
@@ -1136,8 +1136,8 @@
             },
             "require-dev": {
                 "ext-curl": "*",
-                "php-http/client-integration-tests": "dev-phpunit8",
-                "phpunit/phpunit": "^8.5.5",
+                "php-http/client-integration-tests": "^3.0",
+                "phpunit/phpunit": "^8.5.5 || ^9.3.5",
                 "psr/log": "^1.1"
             },
             "suggest": {
@@ -1206,7 +1206,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-09-30T08:51:17+00:00"
+            "time": "2020-10-10T11:47:56+00:00"
         },
         {
             "name": "guzzlehttp/promises",
@@ -1402,16 +1402,16 @@
         },
         {
             "name": "laravel/framework",
-            "version": "v8.9.0",
+            "version": "v8.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54"
+                "reference": "0c80950806cd1bc6d9a7068585a12c2bfa23bdf3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/8a6bf870bcfa1597e514a9c7ee6df44db98abb54",
-                "reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/0c80950806cd1bc6d9a7068585a12c2bfa23bdf3",
+                "reference": "0c80950806cd1bc6d9a7068585a12c2bfa23bdf3",
                 "shasum": ""
             },
             "require": {
@@ -1561,7 +1561,7 @@
                 "framework",
                 "laravel"
             ],
-            "time": "2020-10-06T14:22:36+00:00"
+            "time": "2020-10-13T14:20:53+00:00"
         },
         {
             "name": "laravel/passport",
@@ -2331,30 +2331,30 @@
         },
         {
             "name": "markbaker/complex",
-            "version": "1.5.0",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/MarkBaker/PHPComplex.git",
-                "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2"
+                "reference": "9999f1432fae467bc93c53f357105b4c31bb994c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/c3131244e29c08d44fefb49e0dd35021e9e39dd2",
-                "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2",
+                "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/9999f1432fae467bc93c53f357105b4c31bb994c",
+                "reference": "9999f1432fae467bc93c53f357105b4c31bb994c",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6.0|^7.0"
+                "php": "^7.2 || ^8.0"
             },
             "require-dev": {
-                "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
                 "phpcompatibility/php-compatibility": "^9.0",
                 "phpdocumentor/phpdocumentor": "2.*",
-                "phploc/phploc": "^4.0|^5.0|^6.0|^7.0",
+                "phploc/phploc": "^4.0",
                 "phpmd/phpmd": "2.*",
-                "phpunit/phpunit": "^4.8.35|^5.0|^6.0|^7.0",
-                "sebastian/phpcpd": "2.*",
-                "squizlabs/php_codesniffer": "^3.4.0"
+                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3",
+                "sebastian/phpcpd": "^4.0",
+                "squizlabs/php_codesniffer": "^3.4"
             },
             "type": "library",
             "autoload": {
@@ -2422,33 +2422,34 @@
                 "complex",
                 "mathematics"
             ],
-            "time": "2020-08-26T19:47:57+00:00"
+            "time": "2020-08-26T10:42:07+00:00"
         },
         {
             "name": "markbaker/matrix",
-            "version": "1.2.1",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/MarkBaker/PHPMatrix.git",
-                "reference": "182d44c3b2e3b063468f7481ae3ef71c69dc1409"
+                "reference": "9567d9c4c519fbe40de01dbd1e4469dbbb66f46a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/182d44c3b2e3b063468f7481ae3ef71c69dc1409",
-                "reference": "182d44c3b2e3b063468f7481ae3ef71c69dc1409",
+                "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/9567d9c4c519fbe40de01dbd1e4469dbbb66f46a",
+                "reference": "9567d9c4c519fbe40de01dbd1e4469dbbb66f46a",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6.0|^7.0.0"
+                "php": "^7.2 || ^8.0"
             },
             "require-dev": {
-                "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
-                "phpcompatibility/php-compatibility": "dev-master",
-                "phploc/phploc": "^4",
-                "phpmd/phpmd": "dev-master",
-                "phpunit/phpunit": "^5.7|^6.0|7.0",
-                "sebastian/phpcpd": "^3.0",
-                "squizlabs/php_codesniffer": "^3.0@dev"
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+                "phpcompatibility/php-compatibility": "^9.0",
+                "phpdocumentor/phpdocumentor": "2.*",
+                "phploc/phploc": "^4.0",
+                "phpmd/phpmd": "2.*",
+                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3",
+                "sebastian/phpcpd": "^4.0",
+                "squizlabs/php_codesniffer": "^3.4"
             },
             "type": "library",
             "autoload": {
@@ -2481,7 +2482,7 @@
             "authors": [
                 {
                     "name": "Mark Baker",
-                    "email": "mark@lange.demon.co.uk"
+                    "email": "mark@demon-angel.eu"
                 }
             ],
             "description": "PHP Class for working with matrices",
@@ -2491,7 +2492,7 @@
                 "matrix",
                 "vector"
             ],
-            "time": "2020-08-28T19:41:55+00:00"
+            "time": "2020-08-28T17:11:00+00:00"
         },
         {
             "name": "mews/captcha",
@@ -2701,16 +2702,16 @@
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.41.0",
+            "version": "2.41.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "8690b13ad4da6d54d692afea15aab30b36fee52e"
+                "reference": "e148788eeae9b9b7b87996520358b86faad37b52"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/8690b13ad4da6d54d692afea15aab30b36fee52e",
-                "reference": "8690b13ad4da6d54d692afea15aab30b36fee52e",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e148788eeae9b9b7b87996520358b86faad37b52",
+                "reference": "e148788eeae9b9b7b87996520358b86faad37b52",
                 "shasum": ""
             },
             "require": {
@@ -2786,7 +2787,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-10-04T09:11:05+00:00"
+            "time": "2020-10-12T20:36:09+00:00"
         },
         {
             "name": "nikic/php-parser",
@@ -2915,29 +2916,29 @@
         },
         {
             "name": "opis/closure",
-            "version": "3.5.7",
+            "version": "3.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/opis/closure.git",
-                "reference": "4531e53afe2fc660403e76fb7644e95998bff7bf"
+                "reference": "c547f8262a5fa9ff507bd06cc394067b83a75085"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/opis/closure/zipball/4531e53afe2fc660403e76fb7644e95998bff7bf",
-                "reference": "4531e53afe2fc660403e76fb7644e95998bff7bf",
+                "url": "https://api.github.com/repos/opis/closure/zipball/c547f8262a5fa9ff507bd06cc394067b83a75085",
+                "reference": "c547f8262a5fa9ff507bd06cc394067b83a75085",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.4 || ^7.0"
+                "php": "^5.4 || ^7.0 || ^8.0"
             },
             "require-dev": {
                 "jeremeamia/superclosure": "^2.0",
-                "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+                "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.5.x-dev"
+                    "dev-master": "3.6.x-dev"
                 }
             },
             "autoload": {
@@ -2972,7 +2973,7 @@
                 "serialization",
                 "serialize"
             ],
-            "time": "2020-09-06T17:02:15+00:00"
+            "time": "2020-10-11T21:42:15+00:00"
         },
         {
             "name": "paragonie/constant_time_encoding",
@@ -3038,20 +3039,20 @@
         },
         {
             "name": "paragonie/random_compat",
-            "version": "v9.99.99",
+            "version": "v9.99.100",
             "source": {
                 "type": "git",
                 "url": "https://github.com/paragonie/random_compat.git",
-                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+                "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
-                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+                "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
                 "shasum": ""
             },
             "require": {
-                "php": "^7"
+                "php": ">= 7"
             },
             "require-dev": {
                 "phpunit/phpunit": "4.*|5.*",
@@ -3079,7 +3080,7 @@
                 "pseudorandom",
                 "random"
             ],
-            "time": "2018-07-02T15:55:56+00:00"
+            "time": "2020-10-15T08:29:30+00:00"
         },
         {
             "name": "php-http/message-factory",
@@ -3215,16 +3216,16 @@
         },
         {
             "name": "phpoffice/phpspreadsheet",
-            "version": "1.14.1",
+            "version": "1.15.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
-                "reference": "2383aad5689778470491581442aab38cec41bf1d"
+                "reference": "a8e8068b31b8119e1daa5b1eb5715a3a8ea8305f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/2383aad5689778470491581442aab38cec41bf1d",
-                "reference": "2383aad5689778470491581442aab38cec41bf1d",
+                "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/a8e8068b31b8119e1daa5b1eb5715a3a8ea8305f",
+                "reference": "a8e8068b31b8119e1daa5b1eb5715a3a8ea8305f",
                 "shasum": ""
             },
             "require": {
@@ -3242,9 +3243,9 @@
                 "ext-zip": "*",
                 "ext-zlib": "*",
                 "maennchen/zipstream-php": "^2.1",
-                "markbaker/complex": "^1.4",
-                "markbaker/matrix": "^1.2",
-                "php": "^7.2",
+                "markbaker/complex": "^1.5|^2.0",
+                "markbaker/matrix": "^1.2|^2.0",
+                "php": "^7.2|^8.0",
                 "psr/http-client": "^1.0",
                 "psr/http-factory": "^1.0",
                 "psr/simple-cache": "^1.0"
@@ -3255,15 +3256,15 @@
                 "jpgraph/jpgraph": "^4.0",
                 "mpdf/mpdf": "^8.0",
                 "phpcompatibility/php-compatibility": "^9.3",
-                "phpunit/phpunit": "^8.5",
+                "phpunit/phpunit": "^8.5|^9.3",
                 "squizlabs/php_codesniffer": "^3.5",
                 "tecnickcom/tcpdf": "^6.3"
             },
             "suggest": {
-                "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+                "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)",
                 "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
                 "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
-                "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+                "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)"
             },
             "type": "library",
             "autoload": {
@@ -3307,7 +3308,7 @@
                 "xls",
                 "xlsx"
             ],
-            "time": "2020-07-19T09:51:35+00:00"
+            "time": "2020-10-11T13:20:59+00:00"
         },
         {
             "name": "phpoption/phpoption",
@@ -3656,6 +3657,130 @@
             ],
             "time": "2019-03-20T16:42:58+00:00"
         },
+        {
+            "name": "pragmarx/version",
+            "version": "v1.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/antonioribeiro/version.git",
+                "reference": "691fd35c94eea9d58a9851f8fdb6c2dcee85af9a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/antonioribeiro/version/zipball/691fd35c94eea9d58a9851f8fdb6c2dcee85af9a",
+                "reference": "691fd35c94eea9d58a9851f8fdb6c2dcee85af9a",
+                "shasum": ""
+            },
+            "require": {
+                "laravel/framework": ">=5.5.33",
+                "php": "^7.0",
+                "pragmarx/yaml": "^1.0",
+                "symfony/process": "^3.3|^4.0|^5.0"
+            },
+            "require-dev": {
+                "orchestra/testbench": "3.4.*|3.5.*|3.6.*|3.7.*|4.*|5.*",
+                "phpunit/phpunit": "~5|~6|~7|~8|~9"
+            },
+            "type": "library",
+            "extra": {
+                "component": "package",
+                "laravel": {
+                    "providers": [
+                        "PragmaRX\\Version\\Package\\ServiceProvider"
+                    ],
+                    "aliases": {
+                        "Version": "PragmaRX\\Version\\Package\\Facade"
+                    }
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PragmaRX\\Version\\Package\\": "src/package",
+                    "PragmaRX\\Version\\Tests\\": "tests/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Antonio Carlos Ribeiro",
+                    "email": "acr@antoniocarlosribeiro.com",
+                    "role": "Creator & Designer"
+                }
+            ],
+            "description": "Take control over your Laravel app version",
+            "keywords": [
+                "laravel",
+                "version",
+                "versioning"
+            ],
+            "time": "2020-09-14T10:31:27+00:00"
+        },
+        {
+            "name": "pragmarx/yaml",
+            "version": "v1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/antonioribeiro/yaml.git",
+                "reference": "9f2b44c4a31f8a71bab77169205aee7184ae3ef4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/antonioribeiro/yaml/zipball/9f2b44c4a31f8a71bab77169205aee7184ae3ef4",
+                "reference": "9f2b44c4a31f8a71bab77169205aee7184ae3ef4",
+                "shasum": ""
+            },
+            "require": {
+                "illuminate/support": ">=5.5.33",
+                "php": ">=7.0",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
+            },
+            "require-dev": {
+                "orchestra/testbench": "3.5|^3.6|^4.0|^5.0",
+                "phpunit/phpunit": "^4.0|^6.4|^7.0|^8.0|^9.0"
+            },
+            "suggest": {
+                "ext-yaml": "Required to use the PECL YAML."
+            },
+            "type": "library",
+            "extra": {
+                "component": "package",
+                "laravel": {
+                    "providers": [
+                        "PragmaRX\\Yaml\\Package\\ServiceProvider"
+                    ],
+                    "aliases": {
+                        "Yaml": "PragmaRX\\Yaml\\Package\\Facade"
+                    }
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PragmaRX\\Yaml\\Package\\": "src/package",
+                    "PragmaRX\\Yaml\\Tests\\": "tests/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Antonio Carlos Ribeiro",
+                    "email": "acr@antoniocarlosribeiro.com",
+                    "role": "Creator & Designer"
+                }
+            ],
+            "description": "Load your Laravel config files using yaml",
+            "keywords": [
+                "config",
+                "laravel",
+                "yaml"
+            ],
+            "time": "2020-05-21T19:46:28+00:00"
+        },
         {
             "name": "predis/predis",
             "version": "v1.1.6",
@@ -4925,16 +5050,16 @@
         },
         {
             "name": "symfony/http-client-contracts",
-            "version": "v2.2.0",
+            "version": "v2.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-client-contracts.git",
-                "reference": "3a5d0fe7908daaa23e3dbf4cee3ba4bfbb19fdd3"
+                "reference": "41db680a15018f9c1d4b23516059633ce280ca33"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/3a5d0fe7908daaa23e3dbf4cee3ba4bfbb19fdd3",
-                "reference": "3a5d0fe7908daaa23e3dbf4cee3ba4bfbb19fdd3",
+                "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33",
+                "reference": "41db680a15018f9c1d4b23516059633ce280ca33",
                 "shasum": ""
             },
             "require": {
@@ -4945,8 +5070,9 @@
             },
             "type": "library",
             "extra": {
+                "branch-version": "2.3",
                 "branch-alias": {
-                    "dev-master": "2.2-dev"
+                    "dev-main": "2.3-dev"
                 },
                 "thanks": {
                     "name": "symfony/contracts",
@@ -4996,7 +5122,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-07T11:33:47+00:00"
+            "time": "2020-10-14T17:08:19+00:00"
         },
         {
             "name": "symfony/http-foundation",
@@ -6696,6 +6822,83 @@
             ],
             "time": "2020-09-18T14:27:32+00:00"
         },
+        {
+            "name": "symfony/yaml",
+            "version": "v5.1.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/yaml.git",
+                "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a",
+                "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2.5",
+                "symfony/deprecation-contracts": "^2.1",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "conflict": {
+                "symfony/console": "<4.4"
+            },
+            "require-dev": {
+                "symfony/console": "^4.4|^5.0"
+            },
+            "suggest": {
+                "symfony/console": "For validating YAML files using the lint command"
+            },
+            "bin": [
+                "Resources/bin/yaml-lint"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Yaml\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Yaml Component",
+            "homepage": "https://symfony.com",
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-09-27T03:44:28+00:00"
+        },
         {
             "name": "tijsverkoyen/css-to-inline-styles",
             "version": "2.2.3",
@@ -7290,16 +7493,16 @@
         },
         {
             "name": "facade/ignition",
-            "version": "2.3.8",
+            "version": "2.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/facade/ignition.git",
-                "reference": "e8fed9c382cd1d02b5606688576a35619afdf82c"
+                "reference": "9fc6c3d3de5271a1b94cff19dce2c9295abf0ffa"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/facade/ignition/zipball/e8fed9c382cd1d02b5606688576a35619afdf82c",
-                "reference": "e8fed9c382cd1d02b5606688576a35619afdf82c",
+                "url": "https://api.github.com/repos/facade/ignition/zipball/9fc6c3d3de5271a1b94cff19dce2c9295abf0ffa",
+                "reference": "9fc6c3d3de5271a1b94cff19dce2c9295abf0ffa",
                 "shasum": ""
             },
             "require": {
@@ -7358,29 +7561,29 @@
                 "laravel",
                 "page"
             ],
-            "time": "2020-10-01T23:01:14+00:00"
+            "time": "2020-10-14T08:59:59+00:00"
         },
         {
             "name": "facade/ignition-contracts",
-            "version": "1.0.1",
+            "version": "1.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/facade/ignition-contracts.git",
-                "reference": "aeab1ce8b68b188a43e81758e750151ad7da796b"
+                "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/aeab1ce8b68b188a43e81758e750151ad7da796b",
-                "reference": "aeab1ce8b68b188a43e81758e750151ad7da796b",
+                "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267",
+                "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1"
+                "php": "^7.3|^8.0"
             },
             "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.14",
-                "phpunit/phpunit": "^7.5|^8.0",
-                "vimeo/psalm": "^3.12"
+                "friendsofphp/php-cs-fixer": "^v2.15.8",
+                "phpunit/phpunit": "^9.3.11",
+                "vimeo/psalm": "^3.17.1"
             },
             "type": "library",
             "autoload": {
@@ -7407,7 +7610,7 @@
                 "flare",
                 "ignition"
             ],
-            "time": "2020-07-14T10:10:28+00:00"
+            "time": "2020-10-16T08:27:54+00:00"
         },
         {
             "name": "filp/whoops",
@@ -7975,23 +8178,23 @@
         },
         {
             "name": "php-cs-fixer/diff",
-            "version": "v1.3.0",
+            "version": "v1.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/PHP-CS-Fixer/diff.git",
-                "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756"
+                "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/78bb099e9c16361126c86ce82ec4405ebab8e756",
-                "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756",
+                "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/dbd31aeb251639ac0b9e7e29405c1441907f5759",
+                "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "php": "^5.6 || ^7.0 || ^8.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.7.23 || ^6.4.3",
+                "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0",
                 "symfony/process": "^3.3"
             },
             "type": "library",
@@ -8005,14 +8208,14 @@
                 "BSD-3-Clause"
             ],
             "authors": [
-                {
-                    "name": "Kore Nordmann",
-                    "email": "mail@kore-nordmann.de"
-                },
                 {
                     "name": "Sebastian Bergmann",
                     "email": "sebastian@phpunit.de"
                 },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "mail@kore-nordmann.de"
+                },
                 {
                     "name": "SpacePossum"
                 }
@@ -8022,7 +8225,7 @@
             "keywords": [
                 "diff"
             ],
-            "time": "2018-02-15T16:58:55+00:00"
+            "time": "2020-10-14T08:39:05+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
@@ -8533,16 +8736,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.4.0",
+            "version": "9.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "ef533467a7974c4b6c354f3eff42a115910bd4e5"
+                "reference": "1f09a12726593737e8a228ebb1c8647305d07c41"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ef533467a7974c4b6c354f3eff42a115910bd4e5",
-                "reference": "ef533467a7974c4b6c354f3eff42a115910bd4e5",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1f09a12726593737e8a228ebb1c8647305d07c41",
+                "reference": "1f09a12726593737e8a228ebb1c8647305d07c41",
                 "shasum": ""
             },
             "require": {
@@ -8557,23 +8760,23 @@
                 "phar-io/manifest": "^2.0.1",
                 "phar-io/version": "^3.0.2",
                 "php": ">=7.3",
-                "phpspec/prophecy": "^1.11.1",
+                "phpspec/prophecy": "^1.12.1",
                 "phpunit/php-code-coverage": "^9.2",
-                "phpunit/php-file-iterator": "^3.0.4",
-                "phpunit/php-invoker": "^3.1",
-                "phpunit/php-text-template": "^2.0.2",
-                "phpunit/php-timer": "^5.0.1",
-                "sebastian/cli-parser": "^1.0",
-                "sebastian/code-unit": "^1.0.5",
-                "sebastian/comparator": "^4.0.3",
-                "sebastian/diff": "^4.0.2",
-                "sebastian/environment": "^5.1.2",
-                "sebastian/exporter": "^4.0.2",
-                "sebastian/global-state": "^5.0",
-                "sebastian/object-enumerator": "^4.0.2",
-                "sebastian/resource-operations": "^3.0.2",
-                "sebastian/type": "^2.2.1",
-                "sebastian/version": "^3.0.1"
+                "phpunit/php-file-iterator": "^3.0.5",
+                "phpunit/php-invoker": "^3.1.1",
+                "phpunit/php-text-template": "^2.0.3",
+                "phpunit/php-timer": "^5.0.2",
+                "sebastian/cli-parser": "^1.0.1",
+                "sebastian/code-unit": "^1.0.6",
+                "sebastian/comparator": "^4.0.5",
+                "sebastian/diff": "^4.0.3",
+                "sebastian/environment": "^5.1.3",
+                "sebastian/exporter": "^4.0.3",
+                "sebastian/global-state": "^5.0.1",
+                "sebastian/object-enumerator": "^4.0.3",
+                "sebastian/resource-operations": "^3.0.3",
+                "sebastian/type": "^2.3",
+                "sebastian/version": "^3.0.2"
             },
             "require-dev": {
                 "ext-pdo": "*",
@@ -8628,7 +8831,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-10-02T03:54:37+00:00"
+            "time": "2020-10-11T07:41:19+00:00"
         },
         {
             "name": "scrivo/highlight.php",

+ 1 - 1
config/anonaddy.php

@@ -155,7 +155,7 @@ return [
     |
     */
 
-    'signing_key_fingerprint' => env('ANONADDY_SIGNING_KEY_FINGERPRINT'),
+    'signing_key_fingerprint' => env('ANONADDY_SIGNING_KEY_FINGERPRINT', null),
 
     /*
     |--------------------------------------------------------------------------

+ 60 - 0
config/version.yml

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

+ 1 - 0
database/factories/UserFactory.php

@@ -27,6 +27,7 @@ class UserFactory extends Factory
             'username' => $this->faker->userName.$this->faker->randomNumber(3),
             'banner_location' => 'top',
             'bandwidth' => 0,
+            'catch_all' => 1,
             'default_recipient_id' => Recipient::factory(),
             'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
             'remember_token' => Str::random(10),

+ 32 - 0
database/migrations/2020_10_13_091421_add_catch_all_to_users_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddCatchAllToUsersTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->boolean('catch_all')->after('banner_location')->default(true);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn('catch_all');
+        });
+    }
+}

+ 24 - 24
package-lock.json

@@ -2710,9 +2710,9 @@
             "dev": true
         },
         "dayjs": {
-            "version": "1.9.1",
-            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.1.tgz",
-            "integrity": "sha512-01NCTBg8cuMJG1OQc6PR7T66+AFYiPwgDvdJmvJBn29NGzIG+DIFxPLNjHzwz3cpFIvG+NcwIjP9hSaPVoOaDg=="
+            "version": "1.9.3",
+            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.3.tgz",
+            "integrity": "sha512-V+1SyIvkS+HmNbN1G7A9+ERbFTV9KTXu6Oor98v2xHmzzpp52OIJhQuJSTywWuBY5pyAEmlwbCi1Me87n/SLOw=="
         },
         "de-indent": {
             "version": "1.0.2",
@@ -3155,9 +3155,9 @@
             }
         },
         "escalade": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz",
-            "integrity": "sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig=="
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+            "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
         },
         "escape-html": {
             "version": "1.0.3",
@@ -8840,9 +8840,9 @@
             }
         },
         "sortablejs": {
-            "version": "1.10.2",
-            "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
-            "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
+            "version": "1.12.0",
+            "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.12.0.tgz",
+            "integrity": "sha512-bPn57rCjBRlt2sC24RBsu40wZsmLkSo2XeqG8k6DC1zru5eObQUIPPZAQG7W2SJ8FZQYq+BEJmvuw1Zxb3chqg=="
         },
         "source-list-map": {
             "version": "2.0.1",
@@ -9425,9 +9425,9 @@
             "dev": true
         },
         "tailwindcss": {
-            "version": "1.8.12",
-            "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.8.12.tgz",
-            "integrity": "sha512-VChYp+4SduP8hHFAmf75P5Yf0qNQi3oSSnpEMKkC6kWW/9K+SizRgbmllqLJLnTZq+eM3TDwvn1jWXvvg+dfDA==",
+            "version": "1.9.2",
+            "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.9.2.tgz",
+            "integrity": "sha512-D3uKSZZkh4GaKiZWmPEfNrqEmEuYdwaqXOQ7trYSQQFI5laSD9+b2FUUj5g39nk5R1omKp5tBW9wZsfJq+KIVA==",
             "requires": {
                 "@fullhuman/postcss-purgecss": "^2.1.2",
                 "autoprefixer": "^9.4.5",
@@ -9473,9 +9473,9 @@
                     }
                 },
                 "caniuse-lite": {
-                    "version": "1.0.30001146",
-                    "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz",
-                    "integrity": "sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug=="
+                    "version": "1.0.30001148",
+                    "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001148.tgz",
+                    "integrity": "sha512-E66qcd0KMKZHNJQt9hiLZGE3J4zuTqE1OnU53miEVtylFbwOEmeA5OsRu90noZful+XGSQOni1aT2tiqu/9yYw=="
                 },
                 "chalk": {
                     "version": "4.1.0",
@@ -9505,9 +9505,9 @@
                     "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
                 },
                 "electron-to-chromium": {
-                    "version": "1.3.578",
-                    "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz",
-                    "integrity": "sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q=="
+                    "version": "1.3.582",
+                    "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.582.tgz",
+                    "integrity": "sha512-0nCJ7cSqnkMC+kUuPs0YgklFHraWGl/xHqtZWWtOeVtyi+YqkoAOMGuZQad43DscXCQI/yizcTa3u6B5r+BLww=="
                 },
                 "fs-extra": {
                     "version": "8.1.0",
@@ -9525,9 +9525,9 @@
                     "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
                 },
                 "node-releases": {
-                    "version": "1.1.61",
-                    "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz",
-                    "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g=="
+                    "version": "1.1.63",
+                    "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.63.tgz",
+                    "integrity": "sha512-ukW3iCfQaoxJkSPN+iK7KznTeqDGVJatAEuXsJERYHa9tn/KaT5lBdIyxQjLEVTzSkyjJEuQ17/vaEjrOauDkg=="
                 },
                 "postcss-selector-parser": {
                     "version": "6.0.4",
@@ -10130,9 +10130,9 @@
             "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw=="
         },
         "vuedraggable": {
-            "version": "2.24.1",
-            "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.1.tgz",
-            "integrity": "sha512-G1fxO1oshx+WLdieSGl6jSJdlHOQFga1FpjuUpgXldbpKNzxpjsGn4xYNnRHVrOAqm8aG5FfpdQlh5LHesxCeA==",
+            "version": "2.24.2",
+            "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.2.tgz",
+            "integrity": "sha512-y1NbVhLFOVHHdJl7qsYOtExiTq4zyxF+PxiF9NC8kHEtI6sAFhUHtHYp+ONa8v4S3bAspzGHOHuOq0pNO4fFtA==",
             "requires": {
                 "sortablejs": "^1.10.1"
             }

+ 3 - 3
package.json

@@ -13,7 +13,7 @@
     "dependencies": {
         "axios": "^0.18.1",
         "cross-env": "^5.2.1",
-        "dayjs": "^1.9.1",
+        "dayjs": "^1.9.3",
         "laravel-mix": "^4.1.4",
         "laravel-mix-purgecss": "^4.2.0",
         "lodash": "^4.17.20",
@@ -21,7 +21,7 @@
         "postcss-import": "^11.1.0",
         "postcss-nesting": "^5.0.0",
         "resolve-url-loader": "^2.3.2",
-        "tailwindcss": "^1.8.12",
+        "tailwindcss": "^1.9.2",
         "tippy.js": "^4.3.5",
         "v-clipboard": "^2.2.3",
         "vue": "^2.6.12",
@@ -29,7 +29,7 @@
         "vue-multiselect": "^2.1.6",
         "vue-notification": "^1.3.20",
         "vue-template-compiler": "^2.6.12",
-        "vuedraggable": "^2.24.1"
+        "vuedraggable": "^2.24.2"
     },
     "devDependencies": {
         "husky": "^2.7.0",

+ 45 - 0
resources/views/settings/show.blade.php

@@ -393,6 +393,51 @@
 
         <div class="px-6 py-8 md:p-10 bg-white rounded-lg shadow mb-10">
 
+            <form class="mb-16" method="POST" action="{{ route('settings.catch_all') }}">
+                @csrf
+
+                <div class="mb-6">
+
+                    <h3 class="font-bold text-xl">
+                        Update Catch-All Functionality for Account Username
+                    </h3>
+
+                    <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
+
+                    <p class="mt-6">This will determine if your main account username (<b>{{ $user->username }}</b>) is able to function as a catch-all subdomain. When enabled you will be able to create any alias at {{ $user->username }}.{{ config('anonaddy.domain') }} or any of your other subdomains on-the-fly. Meaning they will be created automatically in your dashboard as soon as they receive their first email.
+                    </p>
+                    <p class="mt-4">When disabled you will only be able to receive email for your unique username subdomains if an alias <b>already exists</b> in your account.</p>
+
+                    <div class="mt-6 flex flex-wrap mb-4">
+                        <label for="catch_all" class="block text-grey-700 text-sm mb-2">
+                            {{ __('Update Catch-All') }}:
+                        </label>
+
+                        <div class="block relative w-full">
+                            <select id="catch_all" class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline" name="catch_all" required>
+                                <option value="1" {{ $user->catch_all ? 'selected' : '' }}>Enabled</option>
+                                <option value="0" {{ ! $user->catch_all ? 'selected' : '' }}>Disabled</option>
+                            </select>
+                            <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
+                                <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
+                            </div>
+                        </div>
+
+                        @if ($errors->has('catch_all'))
+                            <p class="text-red-500 text-xs italic mt-4">
+                                {{ $errors->first('catch_all') }}
+                            </p>
+                        @endif
+                    </div>
+
+                </div>
+
+                <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
+                    Update Username Catch-All
+                </button>
+
+            </form>
+
             <form class="mb-16" method="POST" action="{{ route('settings.from_name') }}">
                 @csrf
 

+ 2 - 0
routes/api.php

@@ -79,4 +79,6 @@ Route::group([
     Route::get('/domain-options', 'Api\DomainOptionController@index');
 
     Route::get('/account-details', 'Api\AccountDetailController@index');
+
+    Route::get('/app-version', 'Api\AppVersionController@index');
 });

+ 2 - 0
routes/web.php

@@ -60,6 +60,8 @@ Route::group([
 
     Route::post('/banner-location', 'BannerLocationController@update')->name('settings.banner_location');
 
+    Route::post('/catch-all', 'CatchAllController@update')->name('settings.catch_all');
+
     Route::post('/password', 'PasswordController@update')->name('settings.password');
 
     Route::post('/2fa/enable', 'Auth\TwoFactorAuthController@store')->name('settings.2fa_enable');

+ 28 - 0
tests/Feature/Api/AppVersionTest.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Tests\Feature\Api;
+
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use PragmaRX\Version\Package\Facade as Version;
+use Tests\TestCase;
+
+class AppVersionTest extends TestCase
+{
+    use RefreshDatabase;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        parent::setUpPassport();
+    }
+
+    /** @test */
+    public function user_can_get_app_version()
+    {
+        $response = $this->get('/api/v1/app-version');
+
+        $response->assertSuccessful();
+
+        $this->assertEquals(Version::version(), $response->json()['version']);
+    }
+}

+ 28 - 0
tests/Feature/SettingsTest.php

@@ -210,6 +210,34 @@ class SettingsTest extends TestCase
         $this->assertEquals('top', $this->user->banner_location);
     }
 
+    /** @test */
+    public function user_can_enable_catch_all_for_account()
+    {
+        $this->user->update(['catch_all' => false]);
+
+        $this->assertFalse($this->user->catch_all);
+
+        $response = $this->post('/settings/catch-all/', [
+            'catch_all' => true
+        ]);
+
+        $response->assertStatus(302);
+        $this->assertTrue($this->user->catch_all);
+    }
+
+    /** @test */
+    public function user_can_disable_catch_all_for_account()
+    {
+        $this->assertTrue($this->user->catch_all);
+
+        $response = $this->post('/settings/catch-all/', [
+            'catch_all' => false
+        ]);
+
+        $response->assertStatus(302);
+        $this->assertFalse($this->user->catch_all);
+    }
+
     /** @test */
     public function user_can_delete_account()
     {