diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..506bd57e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: bash +sudo: enabled + +env: + global: + - NONINTERACTIVE=1 + - PUBLIC_IP=auto + - PUBLIC_IPV6=auto + - PRIMARY_HOSTNAME=auto + - SKIP_NETWORK_CHECKS=1 + +before_script: + - sudo sed -i "s/^127.0.1.1.*/127.0.1.1\t$HOSTNAME.example.com\t$HOSTNAME/" /etc/hosts + - sudo apt-get -yqq update + - sudo apt-get -yqq dist-upgrade + - sudo rm -f /opt/jdk_switcher/jdk_switcher.sh + +script: + - sudo setup/start.sh + - curl -IkL "https://$HOSTNAME/" + - curl -IkL "https://$HOSTNAME/admin" + - curl -IkL "https://$HOSTNAME/mail/" + - curl -IkL "https://$HOSTNAME/cloud/" + - bash -c 'shopt -s globstar; shellcheck -s bash **/*.sh || true' + # - cd test + # - pip install -r requirements.txt + # - pytest diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2efdfdb9..fa1f29f5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,13 +2,13 @@ Mail-in-a-Box is an open source community project about working, as a group, to empower ourselves and others to have control over our own digital communications. Just as we hope to increase technological diversity on the Internet through decentralization, we also believe that diverse viewpoints and voices among our community members foster innovation and creative solutions to the challenges we face. -We are committed to providing a safe, welcoming, and harrassment-free space for collaboration, for everyone, without regard to age, disability, economic situation, ethnicity, gender identity and expression, language fluency, level of knowledge or experience, nationality, personal appearance, race, religion, sexual identity and orientation, or any other attribute. Community comes first. This policy supersedes all other project goals. +We are committed to providing a safe, welcoming, and harassment-free space for collaboration, for everyone, without regard to age, disability, economic situation, ethnicity, gender identity and expression, language fluency, level of knowledge or experience, nationality, personal appearance, race, religion, sexual identity and orientation, or any other attribute. Community comes first. This policy supersedes all other project goals. The maintainers of Mail-in-a-Box share the dual responsibility of leading by example and enforcing these policies as necessary to maintain an open and welcoming environment. All community members should be excellent to each other. ## Scope -This Code of Conduct applies to all places where Mail-in-a-Box community activity is ocurring, including on GitHub, in discussion forums, on Slack, on social media, and in real life. The Code of Conduct applies not only on websites/at events run by the Mail-in-a-Box community (e.g. our GitHub organization, our Slack team) but also at any other location where the Mail-in-a-Box community is present (e.g. in issues of other GitHub organizations where Mail-in-a-Box community members are discussing problems related to Mail-in-a-Box, or real-life professional conferences), or whenever a Mail-in-a-Box community member is representing Mail-in-a-Box to the public at large or acting on behalf of Mail-in-a-Box. +This Code of Conduct applies to all places where Mail-in-a-Box community activity is occurring, including on GitHub, in discussion forums, on Slack, on social media, and in real life. The Code of Conduct applies not only on websites/at events run by the Mail-in-a-Box community (e.g. our GitHub organization, our Slack team) but also at any other location where the Mail-in-a-Box community is present (e.g. in issues of other GitHub organizations where Mail-in-a-Box community members are discussing problems related to Mail-in-a-Box, or real-life professional conferences), or whenever a Mail-in-a-Box community member is representing Mail-in-a-Box to the public at large or acting on behalf of Mail-in-a-Box. This code does not apply to activity on a server running Mail-in-a-Box software, unless your server is hosting a service for the Mail-in-a-Box community at large. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00e15ec7..1181b607 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,15 +16,17 @@ With Vagrant set up, the following should boot up Mail-in-a-Box inside a virtual $ vagrant up --provision -_If you're seeing an error message about your *IP address being listed in the Spamhaus Block List*, simply uncomment the `export SKIP_NETWORK_CHECKS=1` line in `Vagrantfile`. It's normal, you're probably using a dynamic IP address assigned by your Internet provider–they're almost all listed._ +_If you are seeing an error message about your *IP address being listed in the Spamhaus Block List*, simply uncomment the `export SKIP_NETWORK_CHECKS=1` line in `Vagrantfile`. It is normal, you are probably using a dynamic IP address assigned by your Internet provider–they are almost all listed._ ### Modifying your `hosts` file -After a while, Mail-in-a-Box will be available at `192.168.50.4` (unless you changed that in your `Vagrantfile`). To be able to use the web-based bits, we recommend to add a hostname to your `hosts` file: +After a while, Mail-in-a-Box will be available at `192.168.50.4` (unless you changed that in your `Vagrantfile`). To be able to use the web-based bits, we recommend adding a hostname to your `hosts` file: - $ echo "192.168.50.4 mailinabox.lan" | sudo tee -a /etc/hosts + $ echo -e "192.168.50.4\tmailinabox.lan" | sudo tee -a /etc/hosts + or + $ sudo sed -i "s/^127.0.1.1.*/192.168.50.4\tmailinabox.lan/" /etc/hosts -You should now be able to navigate to https://mailinabox.lan/admin using your browser. There should be an initial admin user with the name `me@mailinabox.lan` and the password `12345678`. +You should now be able to navigate to https://mailinabox.lan/admin using your browser. There should be an initial admin user with the name `me@mailinabox.lan` and the randomly generated password from the output. ### Making changes @@ -43,7 +45,7 @@ Once inside the VM, you can re-run individual parts of the setup like in this ex ### Tests -Mail-in-a-Box needs more tests. If you're still looking for a way to help out, writing and contributing tests would be a great start! +Mail-in-a-Box needs more tests. If you were still looking for a way to help out, writing and contributing tests would be a great start! ## Public domain diff --git a/README.md b/README.md index d7caacfa..25dde401 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/mail-in-a-box/mailinabox.svg?branch=master)](https://travis-ci.org/mail-in-a-box/mailinabox) + Mail-in-a-Box ============= @@ -13,7 +15,7 @@ Our goals are to: * Make deploying a good mail server easy. * Promote [decentralization](http://redecentralize.org/), innovation, and privacy on the web. -* Have automated, auditable, and [idempotent](https://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration. +* Have automated, auditable and [idempotent](https://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration. * **Not** make a totally unhackable, NSA-proof server. * **Not** make something customizable by power users. @@ -28,11 +30,11 @@ It is a one-click email appliance. There are no user-configurable setup options. The components installed are: -* SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), Exchange ActiveSync ([z-push](http://z-push.org/)) -* Webmail ([Roundcube](http://roundcube.net/)), static website hosting ([nginx](http://nginx.org/)) -* Spam filtering ([spamassassin](https://spamassassin.apache.org/)), greylisting ([postgrey](http://postgrey.schweikert.ch/)) -* DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), and [SSHFP](https://tools.ietf.org/html/rfc4255) records automatically set -* Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), system monitoring ([munin](http://munin-monitoring.org/)) +* SMTP ([Postfix](http://www.postfix.org/)), POP3/IMAP ([Dovecot](https://dovecot.org/)), [CardDAV](https://en.wikipedia.org/wiki/CardDAV)/[CalDAV](https://en.wikipedia.org/wiki/CalDAV) ([Nextcloud](https://nextcloud.com/)), Exchange ActiveSync ([Z-Push](http://z-push.org/)) +* Webmail ([Roundcube](https://roundcube.net/)), static website hosting ([nginx](https://nginx.org/)) +* Spam filtering ([SpamAssassin](https://spamassassin.apache.org/)), greylisting ([Postgrey](http://postgrey.schweikert.ch/)) +* DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC) ([OpenDMARC](http://www.trusteddomain.org/opendmarc/)), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), and [SSHFP](https://tools.ietf.org/html/rfc4255) records automatically set +* Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([UFW](https://launchpad.net/ufw)), intrusion protection ([Fail2Ban](https://www.fail2ban.org/wiki/index.php/Main_Page)), system monitoring ([munin](http://munin-monitoring.org/)) It also includes: @@ -53,8 +55,7 @@ Clone this repository: $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox -_Optional:_ Download my PGP key and then verify that the sources were signed -by me: +_Optional:_ Download my PGP key and then verify that I signed the sources: $ curl -s https://keybase.io/joshdata/key.asc | gpg --import gpg: key C10BDD81: public key "Joshua Tauberer " imported @@ -66,9 +67,9 @@ by me: gpg: There is no indication that the signature belongs to the owner. Primary key fingerprint: 5F4C 0E73 13CC D744 693B 2AEA B920 41F4 C10B DD81 -You'll get a lot of warnings, but that's OK. Check that the primary key fingerprint matches the +You will get many warnings, but that is OK. Check that the primary key fingerprint matches the fingerprint in the key details at [https://keybase.io/joshdata](https://keybase.io/joshdata) -and on my [personal homepage](https://razor.occams.info/). (Of course, if this repository has been compromised you can't trust these instructions.) +and on my [personal homepage](https://razor.occams.info/). (Of course, if this repository has been compromised you cannot trust these instructions.) Checkout the tag corresponding to the most recent release: @@ -80,7 +81,7 @@ Begin the installation. For help, DO NOT contact me directly --- I don't do tech support by email or tweet (no exceptions). -Post your question on the [discussion forum](https://discourse.mailinabox.email/) instead, where me and other Mail-in-a-Box users may be able to help you. +Post your question on the [discussion forum](https://discourse.mailinabox.email/) instead, where I and other Mail-in-a-Box users may be able to help you. Contributing and Development ---------------------------- @@ -98,8 +99,8 @@ Mail-in-a-Box is similar to [iRedMail](http://www.iredmail.org/) and [Modoboa](h The History ----------- -* In 2007 I wrote a relatively popular Mozilla Thunderbird extension that added client-side SPF and DKIM checks to mail to warn users about possible phishing: [add-on page](https://addons.mozilla.org/en-us/thunderbird/addon/sender-verification-anti-phish/), [source](https://github.com/JoshData/thunderbird-spf). -* In August 2013 I began Mail-in-a-Box by combining my own mail server configuration with the setup in ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) and making the setup steps reproducible with bash scripts. +* In 2007, I wrote a relatively popular Mozilla Thunderbird extension that added client-side SPF and DKIM checks to mail to warn users about possible phishing: [add-on page](https://addons.mozilla.org/en-us/thunderbird/addon/sender-verification-anti-phish/), [source](https://github.com/JoshData/thunderbird-spf). +* In August 2013, I began Mail-in-a-Box by combining my own mail server configuration with the setup in ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) and making the setup steps reproducible with bash scripts. * Mail-in-a-Box was a semifinalist in the 2014 [Knight News Challenge](https://www.newschallenge.org/challenge/2014/submissions/mail-in-a-box), but it was not selected as a winner. * Mail-in-a-Box hit the front page of Hacker News in [April](https://news.ycombinator.com/item?id=7634514) 2014, [September](https://news.ycombinator.com/item?id=8276171) 2014, [May](https://news.ycombinator.com/item?id=9624267) 2015, and [November](https://news.ycombinator.com/item?id=13050500) 2016. * FastCompany mentioned Mail-in-a-Box a [roundup of privacy projects](http://www.fastcompany.com/3047645/your-own-private-cloud) on June 26, 2015. diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh index 3054dd33..c9dd754b 100755 --- a/management/daily_tasks.sh +++ b/management/daily_tasks.sh @@ -11,7 +11,7 @@ export LC_TYPE=en_US.UTF-8 # On Mondays, i.e. once a week, send the administrator a report of total emails # sent and received so the admin might notice server abuse. -if [ `date "+%u"` -eq 1 ]; then +if [ "$(date "+%u")" -eq 1 ]; then management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report" fi diff --git a/security.md b/security.md index 5a797673..f820f814 100644 --- a/security.md +++ b/security.md @@ -15,7 +15,7 @@ The primary goal of Mail-in-a-Box is to make deploying a good mail server easy, * Do not have physical access to the box (i.e., we do not aim to protect the box from physical access). * Have not been given Unix accounts on the box (i.e., we assume all users with shell access are trusted). -On the other hand, we do assume that adversaries are performing passive surveillance and, possibly, active man-in-the-middle attacks. And so: +On the other hand, we do assume that adversaries are performing passive surveillance and, possibly, active man-in-the-middle attacks. Therefore: * User credentials are always sent through SSH/TLS, never in the clear, with modern TLS settings. * Outbound mail is sent with the highest level of TLS possible. @@ -41,11 +41,11 @@ The services all follow these rules: * TLS certificates are generated with 2048-bit RSA keys and SHA-256 fingerprints. The box provides a self-signed certificate by default. The [setup guide](https://mailinabox.email/guide.html) explains how to verify the certificate fingerprint on first login. Users are encouraged to replace the certificate with a proper CA-signed one. ([source](setup/ssl.sh)) * Only TLSv1, TLSv1.1 and TLSv1.2 are offered (the older SSL protocols are not offered). * HTTPS, IMAP, and POP track the [Mozilla Intermediate Ciphers Recommendation](https://wiki.mozilla.org/Security/Server_Side_TLS), balancing security with supporting a wide range of mail clients. Diffie-Hellman ciphers use a 2048-bit key for forward secrecy. For more details, see the [output of SSLyze for these ports](tests/tls_results.txt). -* SMTP (port 25) uses the Postfix medium grade ciphers and SMTP Submission (port 587) uses the Postfix high grade ciphers ([more info](http://www.postfix.org/postconf.5.html#smtpd_tls_mandatory_ciphers)). +* SMTP (port 25) uses the Postfix medium grade ciphers and SMTP Submission (port 587) uses the Postfix high-grade ciphers ([more info](http://www.postfix.org/postconf.5.html#smtpd_tls_mandatory_ciphers)). Additionally: -* SMTP Submission (port 587) will not accept user credentials without STARTTLS (true also of SMTP on port 25 in case of client misconfiguration), and the submission port won't accept mail without encryption. The minimum cipher key length is 128 bits. (The box is of course configured not to be an open relay. User credentials are required to send outbound mail.) ([source](setup/mail-postfix.sh)) +* SMTP Submission (port 587) will not accept user credentials without STARTTLS (true also of SMTP on port 25 in case of client misconfiguration), and the submission port will not accept mail without encryption. The minimum cipher key length is 128 bits. (The box is of course configured not to be an open relay. User credentials are required to send outbound mail.) ([source](setup/mail-postfix.sh)) * HTTPS (port 443): The HTTPS Strict Transport Security header is set. A redirect from HTTP to HTTPS is offered. The [Qualys SSL Labs test](https://www.ssllabs.com/ssltest) should report an A+ grade. ([source 1](conf/nginx-ssl.conf), [source 2](conf/nginx.conf)) ### Password Storage @@ -66,20 +66,20 @@ If DNSSEC is enabled at the box's domain name's registrar, the SSHFP record that `fail2ban` provides some protection from brute-force login attacks (repeated logins that guess account passwords) by blocking offending IP addresses at the network level. -The following services are protected: SSH, IMAP (dovecot), SMTP submission (postfix), webmail (roundcube), Nextcloud/CalDAV/CardDAV (over HTTP), and the Mail-in-a-Box control panel & munin (over HTTP). +The following services are protected: SSH, IMAP (Dovecot), SMTP submission (Postfix), webmail (Roundcube), Nextcloud/CalDAV/CardDAV (over HTTP), and the Mail-in-a-Box control panel & munin (over HTTP). -Some other services running on the box may be missing fail2ban filters. +Some other services running on the box may be missing Fail2Ban filters. `fail2ban` only blocks IPv4 addresses, however. If the box has a public IPv6 address, it is not protected from these attacks. Outbound Mail ------------- -The basic protocols of email delivery did not plan for the presence of adversaries on the network. For a number of reasons it is not possible in most cases to guarantee that a connection to a recipient server is secure. +The basic protocols of email delivery did not plan for the presence of adversaries on the network. For a number of reasons, it is not possible in most cases to guarantee that a connection to a recipient server is secure. ### DNSSEC -The first step in resolving the destination server for an email address is performing a DNS look-up for the MX record of the domain name. The box uses a locally-running [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC)-aware nameserver to perform the lookup. If the domain name has DNSSEC enabled, DNSSEC guards against DNS records being tampered with. +The first step in resolving the destination server for an email address is performing a DNS look-up for the MX record of the domain name. The box uses a locally running [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC)-aware nameserver to perform the lookup. If the domain name has DNSSEC enabled, DNSSEC guards against DNS records being tampered with. ### Encryption @@ -87,15 +87,15 @@ The box (along with the vast majority of mail servers) uses [opportunistic encry ### DANE -If the recipient's domain name supports DNSSEC and has published a [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) record, then on-the-wire encryption is forced between the box and the recipient MTA and this encryption is not subject to a man-in-the-middle attack. The TLSA record contains a certificate fingerprint which the receiving MTA (server) must present to the box. ([source](setup/mail-postfix.sh)) +If the recipient's domain name supports DNSSEC and has published a [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) record, then on-the-wire encryption is forced between the box and the recipient MTA and this encryption is not subject to a man-in-the-middle attack. The TLSA record contains a certificate fingerprint, which the receiving MTA (server) must present to the box. ([source](setup/mail-postfix.sh)) ### Domain Policy Records -Domain policy records allow recipient MTAs to detect when the _domain_ part of of the sender address in incoming mail has been spoofed. All outbound mail is signed with [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) and "quarantine" [DMARC](https://en.wikipedia.org/wiki/DMARC) records are automatically set in DNS. Receiving MTAs that implement DMARC will automatically quarantine mail that is "From:" a domain hosted by the box but which was not sent by the box. (Strong [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) records are also automatically set in DNS.) ([source](management/dns_update.py)) +Domain policy records allow recipient MTAs to detect when the _domain_ part of the sender address in incoming mail has been spoofed. All outbound mail is signed with [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) and "quarantine" [DMARC](https://en.wikipedia.org/wiki/DMARC) records are automatically set in DNS. Receiving MTAs that implement DMARC will automatically quarantine mail that is "From:" a domain hosted by the box, but which was not sent by the box. (Strong [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) records are also automatically set in DNS.) ([source](management/dns_update.py)) ### User Policy -While domain policy records prevent other servers from sending mail with a "From:" header that matches a domain hosted on the box (see above), those policy records do not guarnatee that the user portion of the sender email address matches the actual sender. In enterprise environments where the box may host the mail of untrusted users, it is important to guard against users impersonating other users. +While domain policy records prevent other servers from sending mail with a "From:" header that matches a domain hosted on the box (see above), those policy records do not guarantee that the user portion of the sender email address matches the actual sender. In enterprise environments where the box may host the mail of untrusted users, it is important to guard against users impersonating other users. The box restricts the envelope sender address (also called the return path or MAIL FROM address --- this is different from the "From:" header) that users may put into outbound mail. The envelope sender address must be either their own email address (their SMTP login username) or any alias that they are listed as a permitted sender of. (There is currently no restriction on the contents of the "From:" header.) @@ -112,4 +112,4 @@ When DNSSEC is enabled at the box's domain name's registrar, [DANE TLSA](https:/ ### Filters -Incoming mail is run through several filters. Email is bounced if the sender's IP address is listed in the [Spamhaus Zen blacklist](http://www.spamhaus.org/zen/) or if the sender's domain is listed in the [Spamhaus Domain Block List](http://www.spamhaus.org/dbl/). Greylisting (with [postgrey](http://postgrey.schweikert.ch/)) is also used to cut down on spam. ([source](setup/mail-postfix.sh)) +Incoming mail is run through several filters. Email is bounced if the sender's IP address is listed in the [Spamhaus Zen blacklist](http://www.spamhaus.org/zen/) or if the sender's domain is listed in the [Spamhaus Domain Block List](http://www.spamhaus.org/dbl/). Greylisting (with [Postgrey](http://postgrey.schweikert.ch/)) is also used to cut down on spam. ([source](setup/mail-postfix.sh)) diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index f25de9af..f6f8ca0f 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -2,7 +2,8 @@ ######################################################### # This script is intended to be run like this: # -# curl https://mailinabox.email/setup.sh | sudo bash +# wget https://mailinabox.email/setup.sh -qO - | sudo bash -s +# curl -s https://mailinabox.email/setup.sh | sudo bash -s # ######################################################### @@ -10,41 +11,58 @@ if [ -z "$TAG" ]; then TAG=v0.28 fi +if [[ "$#" -ne 0 ]]; then + echo "Usage: \"wget https://mailinabox.email/setup.sh -qO - | sudo bash -s\" or \"curl -s https://mailinabox.email/setup.sh | sudo bash -s\"" >&2 + exit 1 +fi + # Are we running as root? if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root. Did you leave out sudo?" - exit + echo "This script must be run as root. Did you leave out sudo?" >&2 + exit 1 +fi + +# Check if on Linux +if ! echo "$OSTYPE" | grep -iq "linux"; then + echo "Error: This script must be run on Linux." >&2 + exit 1 +fi + +# Check connectivity +if ! ping -q -c 3 mailinabox.email > /dev/null 2>&1; then + echo "Error: Could not reach mailinabox.email, please check your internet connection and run this script again." >&2 + exit 1 fi # Clone the Mail-in-a-Box repository if it doesn't exist. -if [ ! -d $HOME/mailinabox ]; then +if [ ! -d "$HOME/mailinabox" ]; then if [ ! -f /usr/bin/git ]; then - echo Installing git . . . - apt-get -q -q update - DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null + echo "Installing git . . ." + apt-get -qq update + DEBIAN_FRONTEND=noninteractive apt-get -yqq install git < /dev/null echo fi - echo Downloading Mail-in-a-Box $TAG. . . + echo "Downloading Mail-in-a-Box $TAG. . ." git clone \ -b $TAG --depth 1 \ https://github.com/mail-in-a-box/mailinabox \ - $HOME/mailinabox \ - < /dev/null 2> /dev/null + "$HOME/mailinabox" \ + < /dev/null echo fi # Change directory to it. -cd $HOME/mailinabox +cd "$HOME/mailinabox" # Update it. -if [ "$TAG" != `git describe` ]; then - echo Updating Mail-in-a-Box to $TAG . . . +if [ "$TAG" != "$(git describe)" ]; then + echo "Updating Mail-in-a-Box to $TAG . . ." git fetch --depth 1 --force --prune origin tag $TAG if ! git checkout -q $TAG; then - echo "Update failed. Did you modify something in `pwd`?" - exit + echo "Update failed. Did you modify something in $(pwd)?" >&2 + exit 1 fi echo fi diff --git a/setup/dkim.sh b/setup/dkim.sh index 830e9e18..c6e2d8a5 100755 --- a/setup/dkim.sh +++ b/setup/dkim.sh @@ -10,12 +10,12 @@ source setup/functions.sh # load our functions source /etc/mailinabox.conf # load global vars # Install DKIM... -echo Installing OpenDKIM/OpenDMARC... +echo "Installing OpenDKIM/OpenDMARC..." apt_install opendkim opendkim-tools opendmarc # Make sure configuration directories exist. mkdir -p /etc/opendkim; -mkdir -p $STORAGE_ROOT/mail/dkim +mkdir -p "$STORAGE_ROOT/mail/dkim" # Used in InternalHosts and ExternalIgnoreList configuration directives. # Not quite sure why. @@ -47,12 +47,12 @@ fi # such as Google. But they and others use a 2048 bit key, so we'll # do the same. Keys beyond 2048 bits may exceed DNS record limits. if [ ! -f "$STORAGE_ROOT/mail/dkim/mail.private" ]; then - opendkim-genkey -b 2048 -r -s mail -D $STORAGE_ROOT/mail/dkim + opendkim-genkey -b 2048 -r -s mail -D "$STORAGE_ROOT/mail/dkim" fi # Ensure files are owned by the opendkim user and are private otherwise. -chown -R opendkim:opendkim $STORAGE_ROOT/mail/dkim -chmod go-rwx $STORAGE_ROOT/mail/dkim +chown -R opendkim:opendkim "$STORAGE_ROOT/mail/dkim" +chmod go-rwx "$STORAGE_ROOT/mail/dkim" tools/editconf.py /etc/opendmarc.conf -s \ "Syslog=true" \ diff --git a/setup/dns.sh b/setup/dns.sh index 191a3adc..6943f377 100755 --- a/setup/dns.sh +++ b/setup/dns.sh @@ -92,13 +92,13 @@ if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then # ldns-keygen uses /dev/random for generating random numbers by default. # This is slow and unecessary if we ensure /dev/urandom is seeded properly, # so we use /dev/urandom. See system.sh for an explanation. See #596, #115. - KSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -r /dev/urandom -a $algo -b 2048 -k _domain_); + KSK=$(umask 077; cd "$STORAGE_ROOT/dns/dnssec"; ldns-keygen -r /dev/urandom -a $algo -b 2048 -k _domain_); # Now create a Zone-Signing Key (ZSK) which is expected to be # rotated more often than a KSK, although we have no plans to # rotate it (and doing so would be difficult to do without # disturbing DNS availability.) Omit `-k` and use a shorter key length. - ZSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -r /dev/urandom -a $algo -b 1024 _domain_); + ZSK=$(umask 077; cd "$STORAGE_ROOT/dns/dnssec"; ldns-keygen -r /dev/urandom -a $algo -b 1024 _domain_); # These generate two sets of files like: # @@ -110,7 +110,7 @@ if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then # options. So we'll store the names of the files we just generated. # We might have multiple keys down the road. This will identify # what keys are the current keys. - cat > $STORAGE_ROOT/dns/dnssec/$algo.conf << EOF; + cat > "$STORAGE_ROOT/dns/dnssec/$algo.conf" << EOF; KSK=$KSK ZSK=$ZSK EOF @@ -126,7 +126,7 @@ cat > /etc/cron.daily/mailinabox-dnssec << EOF; #!/bin/bash # Mail-in-a-Box # Re-sign any DNS zones with DNSSEC because the signatures expire periodically. -`pwd`/tools/dns_update +$(pwd)/tools/dns_update EOF chmod +x /etc/cron.daily/mailinabox-dnssec diff --git a/setup/firstuser.sh b/setup/firstuser.sh index d24fc370..14fade57 100644 --- a/setup/firstuser.sh +++ b/setup/firstuser.sh @@ -1,6 +1,6 @@ # If there aren't any mail users yet, create one. -if [ -z "`tools/mail.py user`" ]; then - # The outut of "tools/mail.py user" is a list of mail users. If there +if [ -z "$(tools/mail.py user)" ]; then + # The output of "tools/mail.py user" is a list of mail users. If there # aren't any yet, it'll be empty. # If we didn't ask for an email address at the start, do so now. @@ -10,23 +10,23 @@ if [ -z "`tools/mail.py user`" ]; then input_box "Mail Account" \ "Let's create your first mail account. \n\nWhat email address do you want?" \ - me@`get_default_hostname` \ + "me@$(get_default_hostname)" \ EMAIL_ADDR if [ -z "$EMAIL_ADDR" ]; then # user hit ESC/cancel - exit + exit 1 fi while ! management/mailconfig.py validate-email "$EMAIL_ADDR" do input_box "Mail Account" \ "That's not a valid email address. \n\nWhat email address do you want?" \ - $EMAIL_ADDR \ + "$EMAIL_ADDR" \ EMAIL_ADDR if [ -z "$EMAIL_ADDR" ]; then # user hit ESC/cancel - exit + exit 1 fi done @@ -35,9 +35,10 @@ if [ -z "`tools/mail.py user`" ]; then else # Use me@PRIMARY_HOSTNAME EMAIL_ADDR=me@$PRIMARY_HOSTNAME - EMAIL_PW=12345678 + EMAIL_PW=$(openssl rand -base64 8) echo - echo "Creating a new administrative mail account for $EMAIL_ADDR with password $EMAIL_PW." + echo -e "Creating a new administrative mail account for: $EMAIL_ADDR\n\t\t\t\t with password: $EMAIL_PW" + echo "Warning: This is a security risk. Please change the password after your first login." echo fi else @@ -47,11 +48,11 @@ if [ -z "`tools/mail.py user`" ]; then fi # Create the user's mail account. This will ask for a password if none was given above. - tools/mail.py user add $EMAIL_ADDR $EMAIL_PW + tools/mail.py user add "$EMAIL_ADDR" "$EMAIL_PW" # Make it an admin. - hide_output tools/mail.py user make-admin $EMAIL_ADDR + hide_output tools/mail.py user make-admin "$EMAIL_ADDR" # Create an alias to which we'll direct all automatically-created administrative aliases. - tools/mail.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null + tools/mail.py alias add "administrator@$PRIMARY_HOSTNAME" "$EMAIL_ADDR" > /dev/null fi diff --git a/setup/functions.sh b/setup/functions.sh index 75c6821e..07932d3b 100644 --- a/setup/functions.sh +++ b/setup/functions.sh @@ -3,25 +3,27 @@ function hide_output { # and returns a non-zero exit code. # Get a temporary file. - OUTPUT=$(tempfile) + OUTPUT=$(mktemp) # Execute command, redirecting stderr/stdout to the temporary file. - $@ &> $OUTPUT + "$@" &> "$OUTPUT" # If the command failed, show the output that was captured in the temporary file. E=$? if [ $E != 0 ]; then # Something failed. echo - echo FAILED: $@ - echo ----------------------------------------- - cat $OUTPUT - echo ----------------------------------------- + echo "FAILED: $*" + echo "-----------------------------------------" + cat "$OUTPUT" + echo "-----------------------------------------" + # Remove temporary file. + rm -f "$OUTPUT" exit $E fi # Remove temporary file. - rm -f $OUTPUT + rm -f "$OUTPUT" } function apt_get_quiet { @@ -44,8 +46,8 @@ function apt_install { # install' for all of the packages. Calling `dpkg` on each package is slow, # and doesn't affect what we actually do, except in the messages, so let's # not do that anymore. - PACKAGES=$@ - apt_get_quiet install $PACKAGES + PACKAGES=( "$@" ) + apt_get_quiet install "${PACKAGES[@]}" } function apt_add_repository_to_unattended_upgrades { @@ -61,9 +63,9 @@ function get_default_hostname { # Guess the machine's hostname. It should be a fully qualified # domain name suitable for DNS. None of these calls may provide # the right value, but it's the best guess we can make. - set -- $(hostname --fqdn 2>/dev/null || + set -- "$(hostname --fqdn 2>/dev/null || hostname --all-fqdns 2>/dev/null || - hostname 2>/dev/null) + hostname 2>/dev/null)" printf '%s\n' "$1" # return this value } @@ -121,28 +123,28 @@ function get_default_privateip { route=$(ip -$1 -o route get $target | grep -v unreachable) # Parse the address out of the route information. - address=$(echo $route | sed "s/.* src \([^ ]*\).*/\1/") + address=$(echo "$route" | sed "s/.* src \([^ ]*\).*/\1/") if [[ "$1" == "6" && $address == fe80:* ]]; then # For IPv6 link-local addresses, parse the interface out # of the route information and append it with a '%'. - interface=$(echo $route | sed "s/.* dev \([^ ]*\).*/\1/") + interface=$(echo "$route" | sed "s/.* dev \([^ ]*\).*/\1/") address=$address%$interface fi - echo $address + echo "$address" } function ufw_allow { if [ -z "$DISABLE_FIREWALL" ]; then - # ufw has completely unhelpful output - ufw allow $1 > /dev/null; + # UFW has completely unhelpful output + ufw allow "$1" > /dev/null; fi } function restart_service { - hide_output service $1 restart + hide_output service "$1" restart } ## Dialog Functions ## @@ -167,7 +169,7 @@ function input_menu { declare -n result=$4 declare -n result_code=$4_EXITCODE local IFS=^$'\n' - result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 $3) + result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 "$3") result_code=$? } @@ -178,17 +180,17 @@ function wget_verify { HASH=$2 DEST=$3 CHECKSUM="$HASH $DEST" - rm -f $DEST - hide_output wget -O $DEST $URL + rm -f "$DEST" + hide_output wget -O "$DEST" "$URL" if ! echo "$CHECKSUM" | sha1sum --check --strict > /dev/null; then echo "------------------------------------------------------------" echo "Download of $URL did not match expected checksum." echo "Found:" - sha1sum $DEST + sha1sum "$DEST" echo echo "Expected:" echo "$CHECKSUM" - rm -f $DEST + rm -f "$DEST" exit 1 fi } @@ -204,9 +206,9 @@ function git_clone { SUBDIR=$3 TARGETPATH=$4 TMPPATH=/tmp/git-clone-$$ - rm -rf $TMPPATH $TARGETPATH - git clone -q $REPO $TMPPATH || exit 1 - (cd $TMPPATH; git checkout -q $TREEISH;) || exit 1 - mv $TMPPATH/$SUBDIR $TARGETPATH + rm -rf $TMPPATH "$TARGETPATH" + git clone -q "$REPO" $TMPPATH || exit 1 + (cd $TMPPATH; git checkout -q "$TREEISH";) || exit 1 + mv "$TMPPATH/$SUBDIR" "$TARGETPATH" rm -rf $TMPPATH } diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 21343964..72b92a41 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -12,7 +12,7 @@ # mail as defined in a "sieve" script. # # Dovecot's LDA role comes after spam filtering. Postfix hands mail off -# to Spamassassin which in turn hands it off to Dovecot. This all happens +# to SpamAssassin which in turn hands it off to Dovecot. This all happens # using the LMTP protocol. source setup/functions.sh # load our functions @@ -23,7 +23,7 @@ source /etc/mailinabox.conf # load global vars # but dovecot-lucene is packaged by *us* in the Mail-in-a-Box PPA, # not by Ubuntu. -echo "Installing Dovecot (IMAP server)..." +echo "Installing Dovecot (POP3/IMAP server)..." apt_install \ dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sqlite sqlite3 \ dovecot-sieve dovecot-managesieved dovecot-lucene @@ -45,8 +45,8 @@ apt_install \ # - https://www.dovecot.org/list/dovecot/2012-August/137569.html # - https://www.dovecot.org/list/dovecot/2011-December/132455.html tools/editconf.py /etc/dovecot/conf.d/10-master.conf \ - default_process_limit=$(echo "`nproc` * 250" | bc) \ - default_vsz_limit=$(echo "`free -tm | tail -1 | awk '{print $2}'` / 3" | bc)M \ + default_process_limit="$((CPU_CORES * 250))" \ + default_vsz_limit="$(($(free -tm | tail -1 | awk '{print $2}') / 3))M" \ log_path=/var/log/mail.log # The inotify `max_user_instances` default is 128, which constrains @@ -61,7 +61,7 @@ tools/editconf.py /etc/sysctl.conf \ # username part of the user's email address. We'll ensure that no bad domains or email addresses # are created within the management daemon. tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ - mail_location=maildir:$STORAGE_ROOT/mail/mailboxes/%d/%n \ + mail_location="maildir:$STORAGE_ROOT/mail/mailboxes/%d/%n" \ mail_privileged_group=mail \ first_valid_uid=0 @@ -126,7 +126,7 @@ EOF # ### LDA (LMTP) # Enable Dovecot's LDA service with the LMTP protocol. It will listen -# on port 10026, and Spamassassin will be configured to pass mail there. +# on port 10026, and SpamAssassin will be configured to pass mail there. # # The disabled unix socket listener is normally how Postfix and Dovecot # would communicate (see the Postfix setup script for the corresponding @@ -154,7 +154,7 @@ EOF # Setting a `postmaster_address` is required or LMTP won't start. An alias # will be created automatically by our management daemon. tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \ - postmaster_address=postmaster@$PRIMARY_HOSTNAME + postmaster_address="postmaster@$PRIMARY_HOSTNAME" # ### Sieve @@ -163,7 +163,7 @@ tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \ sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins sieve/" /etc/dovecot/conf.d/20-lmtp.conf # Configure sieve. We'll create a global script that moves mail marked -# as spam by Spamassassin into the user's Spam folder. +# as spam by SpamAssassin into the user's Spam folder. # # * `sieve_before`: The path to our global sieve which handles moving spam to the Spam folder. # @@ -202,14 +202,14 @@ chown -R mail:dovecot /etc/dovecot chmod -R o-rwx /etc/dovecot # Ensure mailbox files have a directory that exists and are owned by the mail user. -mkdir -p $STORAGE_ROOT/mail/mailboxes -chown -R mail.mail $STORAGE_ROOT/mail/mailboxes +mkdir -p "$STORAGE_ROOT/mail/mailboxes" +chown -R mail.mail "$STORAGE_ROOT/mail/mailboxes" # Same for the sieve scripts. -mkdir -p $STORAGE_ROOT/mail/sieve -mkdir -p $STORAGE_ROOT/mail/sieve/global_before -mkdir -p $STORAGE_ROOT/mail/sieve/global_after -chown -R mail.mail $STORAGE_ROOT/mail/sieve +mkdir -p "$STORAGE_ROOT/mail/sieve" +mkdir -p "$STORAGE_ROOT/mail/sieve/global_before" +mkdir -p "$STORAGE_ROOT/mail/sieve/global_after" +chown -R mail.mail "$STORAGE_ROOT/mail/sieve" # Allow the IMAP/POP ports in the firewall. ufw_allow imaps diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index ca52edbd..68178803 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -10,11 +10,11 @@ # other servers on the Internet. It is responsible for very # basic email filtering such as by IP address and greylisting, # it checks that the destination address is valid, rewrites -# destinations according to aliases, and passses email on to +# destinations according to aliases, and passes email on to # another service for local mail delivery. # -# The first hop in local mail delivery is to Spamassassin via -# LMTP. Spamassassin then passes mail over to Dovecot for +# The first hop in local mail delivery is to SpamAssassin via +# LMTP. SpamAssassin then passes mail over to Dovecot for # storage in the user's mailbox. # # Postfix also listens on port 587 (SMTP+STARTLS) for @@ -37,7 +37,7 @@ source /etc/mailinabox.conf # load global vars # * `postfix`: The SMTP server. # * `postfix-pcre`: Enables header filtering. # * `postgrey`: A mail policy service that soft-rejects mail the first time -# it is received. Spammers don't usually try agian. Legitimate mail +# it is received. Spammers don't usually try again. Legitimate mail # always will. # * `ca-certificates`: A trust store used to squelch postfix warnings about # untrusted opportunistically-encrypted connections. @@ -46,7 +46,7 @@ source /etc/mailinabox.conf # load global vars # a modified version of postgrey that lets senders whitelisted by dnswl.org # pass through without being greylisted. So please note [dnswl's license terms](https://www.dnswl.org/?page_id=9): # > Every user with more than 100’000 queries per day on the public nameserver -# > infrastructure and every commercial vendor of dnswl.org data (eg through +# > infrastructure and every commercial vendor of dnswl.org data (e.g. through # > anti-spam solutions) must register with dnswl.org and purchase a subscription. echo "Installing Postfix (SMTP server)..." @@ -63,9 +63,9 @@ apt_install postfix postfix-pcre postgrey ca-certificates # * Set the SMTP banner (which must have the hostname first, then anything). tools/editconf.py /etc/postfix/main.cf \ inet_interfaces=all \ - smtp_bind_address=$PRIVATE_IP \ - smtp_bind_address6=$PRIVATE_IPV6 \ - myhostname=$PRIMARY_HOSTNAME\ + smtp_bind_address="$PRIVATE_IP" \ + smtp_bind_address6="$PRIVATE_IPV6" \ + myhostname="$PRIMARY_HOSTNAME"\ smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \ mydestination=localhost @@ -84,7 +84,7 @@ tools/editconf.py /etc/postfix/main.cf \ # * Do not add the OpenDMAC Authentication-Results header. That should only be added # on incoming mail. Omit the OpenDMARC milter by re-setting smtpd_milters to the # OpenDKIM milter only. See dkim.sh. -# * Even though we dont allow auth over non-TLS connections (smtpd_tls_auth_only below, and without auth the client cant +# * Even though we don't allow auth over non-TLS connections (smtpd_tls_auth_only below, and without auth the client cant # send outbound mail), don't allow non-TLS mail submission on this port anyway to prevent accidental misconfiguration. # * Require the best ciphers for incoming connections per http://baldric.net/2013/12/07/tls-ciphers-in-postfix-and-dovecot/. # By putting this setting here we leave opportunistic TLS on incoming mail at default cipher settings (any cipher is better than none). @@ -121,9 +121,9 @@ sed -i "s/PUBLIC_IP/$PUBLIC_IP/" /etc/postfix/outgoing_mail_header_filters tools/editconf.py /etc/postfix/main.cf \ smtpd_tls_security_level=may\ smtpd_tls_auth_only=yes \ - smtpd_tls_cert_file=$STORAGE_ROOT/ssl/ssl_certificate.pem \ - smtpd_tls_key_file=$STORAGE_ROOT/ssl/ssl_private_key.pem \ - smtpd_tls_dh1024_param_file=$STORAGE_ROOT/ssl/dh2048.pem \ + smtpd_tls_cert_file="$STORAGE_ROOT/ssl/ssl_certificate.pem" \ + smtpd_tls_key_file="$STORAGE_ROOT/ssl/ssl_private_key.pem" \ + smtpd_tls_dh1024_param_file="$STORAGE_ROOT/ssl/dh2048.pem" \ smtpd_tls_protocols=\!SSLv2,\!SSLv3 \ smtpd_tls_ciphers=medium \ smtpd_tls_exclude_ciphers=aNULL,RC4 \ @@ -172,14 +172,14 @@ tools/editconf.py /etc/postfix/main.cf \ # ### Incoming Mail -# Pass any incoming mail over to a local delivery agent. Spamassassin +# Pass any incoming mail over to a local delivery agent. SpamAssassin # will act as the LDA agent at first. It is listening on port 10025 -# with LMTP. Spamassassin will pass the mail over to Dovecot after. +# with LMTP. SpamAssassin will pass the mail over to Dovecot after. # # In a basic setup we would pass mail directly to Dovecot by setting # virtual_transport to `lmtp:unix:private/dovecot-lmtp`. # -tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025 +tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:\[127.0.0.1\]:10025 # Who can send mail to us? Some basic filters. # @@ -200,7 +200,7 @@ tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025 # "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC tools/editconf.py /etc/postfix/main.cf \ smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org" \ - smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023" + smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org,reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023" # Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that # Postgrey listens on the same interface (and not IPv6, for instance). diff --git a/setup/mail-users.sh b/setup/mail-users.sh index ef9b8118..ff52d049 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -5,7 +5,7 @@ # # This script configures user authentication for Dovecot # and Postfix (which relies on Dovecot) and destination -# validation by quering an Sqlite3 database of mail users. +# validation by querying an Sqlite3 database of mail users. source setup/functions.sh # load our functions source /etc/mailinabox.conf # load global vars @@ -18,10 +18,10 @@ source /etc/mailinabox.conf # load global vars db_path=$STORAGE_ROOT/mail/users.sqlite # Create an empty database if it doesn't yet exist. -if [ ! -f $db_path ]; then - echo Creating new user database: $db_path; - echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path; - echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path; +if [ ! -f "$db_path" ]; then + echo "Creating new user database: $db_path"; + echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 "$db_path"; + echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; fi # ### User Authentication @@ -128,7 +128,7 @@ EOF # query the aliases table but also the users table when resolving # aliases, i.e. we turn users into aliases from themselves to # themselves. That means users will match in postfix's first query -# before postfix gets to the third query for catch-alls/domain alises. +# before postfix gets to the third query for catch-alls/domain aliases. # # If there is both an alias and a user for the same address either # might be returned by the UNION, so the whole query is wrapped in diff --git a/setup/management.sh b/setup/management.sh index 064906d3..61d18621 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -9,7 +9,7 @@ echo "Installing Mail-in-a-Box system management daemon..." # We used to install management daemon-related Python packages # directly to /usr/local/lib. We moved to a virtualenv because # these packages might conflict with apt-installed packages. -# We may have a lingering version of acme that conflcits with +# We may have a lingering version of acme that conflicts with # certbot, which we're about to install below, so remove it # first. Once acme is installed by an apt package, this might # break the package version and `apt-get install --reinstall python3-acme` @@ -29,7 +29,7 @@ done # # certbot installs EFF's certbot which we use to # provision free TLS certificates. -apt_install duplicity python-pip python-virtualenv certbot +apt_install duplicity python-pip python-virtualenv certbot python-certbot-nginx hide_output pip2 install --upgrade boto # Create a virtualenv for the installation of Python 3 packages @@ -55,9 +55,9 @@ hide_output $venv/bin/pip install --upgrade \ # CONFIGURATION # Create a backup directory and a random key for encrypting backups. -mkdir -p $STORAGE_ROOT/backup -if [ ! -f $STORAGE_ROOT/backup/secret_key.txt ]; then - $(umask 077; openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt) +mkdir -p "$STORAGE_ROOT/backup" +if [ ! -f "$STORAGE_ROOT/backup/secret_key.txt" ]; then + umask 077; openssl rand -base64 2048 > "$STORAGE_ROOT/backup/secret_key.txt" fi @@ -91,11 +91,11 @@ rm -f /usr/local/bin/mailinabox-daemon # old path cat > $inst_dir/start < /etc/cron.d/mailinabox-nightly << EOF; # Mail-in-a-Box --- Do not edit / will be overwritten on update. # Run nightly tasks: backup, status checks. -0 3 * * * root (cd `pwd` && management/daily_tasks.sh) +0 3 * * * root (cd $(pwd) && management/daily_tasks.sh) EOF # Start the management server. diff --git a/setup/munin.sh b/setup/munin.sh index c812dab2..edc6ad7e 100755 --- a/setup/munin.sh +++ b/setup/munin.sh @@ -40,7 +40,7 @@ chown munin. /var/log/munin/munin-cgi-graph.log # ensure munin-node knows the name of this machine # and reduce logging level to warning tools/editconf.py /etc/munin/munin-node.conf -s \ - host_name=$PRIMARY_HOSTNAME \ + host_name="$PRIMARY_HOSTNAME" \ log_level=1 # Update the activated plugins through munin's autoconfiguration. @@ -52,9 +52,9 @@ find /etc/munin/plugins/ -lname /usr/share/munin/plugins/ntp_ -print0 | xargs -0 # Deactivate monitoring of network interfaces that are not up. Otherwise we can get a lot of empty charts. for f in $(find /etc/munin/plugins/ \( -lname /usr/share/munin/plugins/if_ -o -lname /usr/share/munin/plugins/if_err_ -o -lname /usr/share/munin/plugins/bonding_err_ \)); do - IF=$(echo $f | sed s/.*_//); - if ! ifquery $IF >/dev/null 2>/dev/null; then - rm $f; + IF=$(echo "$f" | sed s/.*_//); + if ! ifquery "$IF" >/dev/null 2>/dev/null; then + rm "$f"; fi; done diff --git a/setup/network-checks.sh b/setup/network-checks.sh index 428fa4ca..1fcd2e58 100644 --- a/setup/network-checks.sh +++ b/setup/network-checks.sh @@ -6,15 +6,15 @@ apt_get_quiet install bind9-host sed netcat-openbsd # The user might have chosen a name that was previously in use by a spammer # and will not be able to reliably send mail. Do this after any automatic # choices made above. -if host $PRIMARY_HOSTNAME.dbl.spamhaus.org > /dev/null; then - echo - echo "The hostname you chose '$PRIMARY_HOSTNAME' is listed in the" - echo "Spamhaus Domain Block List. See http://www.spamhaus.org/dbl/" - echo "and http://www.spamhaus.org/query/domain/$PRIMARY_HOSTNAME." - echo - echo "You will not be able to send mail using this domain name, so" - echo "setup cannot continue." - echo +if host "$PRIMARY_HOSTNAME.dbl.spamhaus.org" > /dev/null; then + echo >&2 + echo "The hostname you chose '$PRIMARY_HOSTNAME' is listed in the" >&2 + echo "Spamhaus Domain Block List. See http://www.spamhaus.org/dbl/" >&2 + echo "and http://www.spamhaus.org/query/domain/$PRIMARY_HOSTNAME." >&2 + echo >&2 + echo "You will not be able to send mail using this domain name, so" >&2 + echo "setup cannot continue." >&2 + echo >&2 exit 1 fi @@ -22,19 +22,19 @@ fi # The user might have ended up on an IP address that was previously in use # by a spammer, or the user may be deploying on a residential network. We # will not be able to reliably send mail in these cases. -REVERSED_IPV4=$(echo $PUBLIC_IP | sed "s/\([0-9]*\).\([0-9]*\).\([0-9]*\).\([0-9]*\)/\4.\3.\2.\1/") -if host $REVERSED_IPV4.zen.spamhaus.org > /dev/null; then - echo - echo "The IP address $PUBLIC_IP is listed in the Spamhaus Block List." - echo "See http://www.spamhaus.org/query/ip/$PUBLIC_IP." - echo - echo "You will not be able to send mail using this machine, so setup" - echo "cannot continue." - echo - echo "Associate a different IP address with this machine if possible." - echo "Many residential network IP addresses are listed, so Mail-in-a-Box" - echo "typically cannot be used on a residential Internet connection." - echo +REVERSED_IPV4=$(echo "$PUBLIC_IP" | sed "s/\([0-9]*\).\([0-9]*\).\([0-9]*\).\([0-9]*\)/\4.\3.\2.\1/") +if host "$REVERSED_IPV4.zen.spamhaus.org" > /dev/null; then + echo >&2 + echo "The IP address $PUBLIC_IP is listed in the Spamhaus Block List." >&2 + echo "See http://www.spamhaus.org/query/ip/$PUBLIC_IP." >&2 + echo >&2 + echo "You will not be able to send mail using this machine, so setup" >&2 + echo "cannot continue." >&2 + echo >&2 + echo "Associate a different IP address with this machine if possible." >&2 + echo "Many residential network IP addresses are listed, so Mail-in-a-Box" >&2 + echo "typically cannot be used on a residential Internet connection." >&2 + echo >&2 exit 1 fi @@ -42,16 +42,16 @@ fi # networks block outbound port 25 to prevent their network from sending spam. # See if we can reach one of Google's MTAs with a 5-second timeout. if ! nc -z -w5 aspmx.l.google.com 25; then - echo - echo "Outbound mail (port 25) seems to be blocked by your network." - echo - echo "You will not be able to send mail using this machine, so setup" - echo "cannot continue." - echo - echo "Many residential networks block port 25 to prevent hijacked" - echo "machines from being able to send spam. I just tried to connect" - echo "to Google's mail server on port 25 but the connection did not" - echo "succeed." - echo + echo >&2 + echo "Outbound mail (port 25) seems to be blocked by your network." >&2 + echo >&2 + echo "You will not be able to send mail using this machine, so setup" >&2 + echo "cannot continue." >&2 + echo >&2 + echo "Many residential networks block port 25 to prevent hijacked" >&2 + echo "machines from being able to send spam. I just tried to connect" >&2 + echo "to Google's mail server on port 25 but the connection did not" >&2 + echo "succeed." >&2 + echo >&2 exit 1 fi diff --git a/setup/owncloud.sh b/setup/owncloud.sh index 5825078d..11a54147 100755 --- a/setup/owncloud.sh +++ b/setup/owncloud.sh @@ -9,7 +9,7 @@ source /etc/mailinabox.conf # load global vars echo "Installing Nextcloud (contacts/calendar)..." -# Keep the php5 dependancies for the owncloud upgrades +# Keep the php5 dependencies for the ownCloud upgrades apt_install \ dbconfig-common \ php5-cli php5-sqlite php5-gd php5-imap php5-curl php-pear php-apc curl libapr1 libtool libcurl4-openssl-dev php-xml-parser \ @@ -23,14 +23,14 @@ apt_install php7.0 php7.0-fpm \ # Migrate <= v0.10 setups that stored the ownCloud config.php in /usr/local rather than # in STORAGE_ROOT. Move the file to STORAGE_ROOT. -if [ ! -f $STORAGE_ROOT/owncloud/config.php ] \ +if [ ! -f "$STORAGE_ROOT/owncloud/config.php" ] \ && [ -f /usr/local/lib/owncloud/config/config.php ]; then # Move config.php and symlink back into previous location. echo "Migrating owncloud/config.php to new location." - mv /usr/local/lib/owncloud/config/config.php $STORAGE_ROOT/owncloud/config.php \ + mv /usr/local/lib/owncloud/config/config.php "$STORAGE_ROOT/owncloud/config.php" \ && \ - ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php + ln -sf "$STORAGE_ROOT/owncloud/config.php" /usr/local/lib/owncloud/config/config.php fi InstallNextcloud() { @@ -42,11 +42,11 @@ InstallNextcloud() { echo "Upgrading to Nextcloud version $version" echo - # Remove the current owncloud/Nextcloud + # Remove the current ownCloud/Nextcloud rm -rf /usr/local/lib/owncloud # Download and verify - wget_verify https://download.nextcloud.com/server/releases/nextcloud-$version.zip $hash /tmp/nextcloud.zip + wget_verify "https://download.nextcloud.com/server/releases/nextcloud-$version.zip" "$hash" /tmp/nextcloud.zip # Extract ownCloud/Nextcloud unzip -q /tmp/nextcloud.zip -d /usr/local/lib @@ -54,7 +54,7 @@ InstallNextcloud() { rm -f /tmp/nextcloud.zip # The two apps we actually want are not in Nextcloud core. Download the releases from - # their github repositories. + # their GitHub repositories. mkdir -p /usr/local/lib/owncloud/apps wget_verify https://github.com/nextcloud/contacts/releases/download/v2.1.5/contacts.tar.gz b7460d15f1b78d492ed502d778c0c458d503ba17 /tmp/contacts.tgz @@ -70,44 +70,46 @@ InstallNextcloud() { # Create a symlink to the config.php in STORAGE_ROOT (for upgrades we're restoring the symlink we previously # put in, and in new installs we're creating a symlink and will create the actual config later). - ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php + ln -sf "$STORAGE_ROOT/owncloud/config.php" /usr/local/lib/owncloud/config/config.php # Make sure permissions are correct or the upgrade step won't run. # $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress # that error. - chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud + chown -f -R www-data.www-data "$STORAGE_ROOT/owncloud" /usr/local/lib/owncloud # If this isn't a new installation, immediately run the upgrade script. # Then check for success (0=ok and 3=no upgrade needed, both are success). - if [ -e $STORAGE_ROOT/owncloud/owncloud.db ]; then + if [ -e "$STORAGE_ROOT/owncloud/owncloud.db" ]; then # ownCloud 8.1.1 broke upgrades. It may fail on the first attempt, but # that can be OK. sudo -u www-data php /usr/local/lib/owncloud/occ upgrade - if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then + E=$? + if [ $E -ne 0 ] && [ $E -ne 3 ]; then echo "Trying ownCloud upgrade again to work around ownCloud upgrade bug..." sudo -u www-data php /usr/local/lib/owncloud/occ upgrade - if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi + E=$? + if [ $E -ne 0 ] && [ $E -ne 3 ]; then exit 1; fi sudo -u www-data php /usr/local/lib/owncloud/occ maintenance:mode --off echo "...which seemed to work." fi fi } -# We only install ownCloud intermediate versions to be able to seemlesly upgrade to Nextcloud +# We only install ownCloud intermediate versions to be able to seamlessly upgrade to Nextcloud InstallOwncloud() { version=$1 hash=$2 echo - echo "Upgrading to OwnCloud version $version" + echo "Upgrading to ownCloud version $version" echo - # Remove the current owncloud/Nextcloud + # Remove the current ownCloud/Nextcloud rm -rf /usr/local/lib/owncloud # Download and verify - wget_verify https://download.owncloud.org/community/owncloud-$version.tar.bz2 $hash /tmp/owncloud.tar.bz2 + wget_verify "https://download.owncloud.org/community/owncloud-$version.tar.bz2" "$hash" /tmp/owncloud.tar.bz2 # Extract ownCloud @@ -115,7 +117,7 @@ InstallOwncloud() { rm -f /tmp/owncloud.tar.bz2 # The two apps we actually want are not in Nextcloud core. Download the releases from - # their github repositories. + # their GitHub repositories. mkdir -p /usr/local/lib/owncloud/apps wget_verify https://github.com/owncloud/contacts/releases/download/v1.4.0.0/contacts.tar.gz c1c22d29699456a45db447281682e8bc3f10e3e7 /tmp/contacts.tgz @@ -131,23 +133,25 @@ InstallOwncloud() { # Create a symlink to the config.php in STORAGE_ROOT (for upgrades we're restoring the symlink we previously # put in, and in new installs we're creating a symlink and will create the actual config later). - ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php + ln -sf "$STORAGE_ROOT/owncloud/config.php" /usr/local/lib/owncloud/config/config.php # Make sure permissions are correct or the upgrade step won't run. # $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress # that error. - chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud + chown -f -R www-data.www-data "$STORAGE_ROOT/owncloud" /usr/local/lib/owncloud # If this isn't a new installation, immediately run the upgrade script. # Then check for success (0=ok and 3=no upgrade needed, both are success). - if [ -e $STORAGE_ROOT/owncloud/owncloud.db ]; then + if [ -e "$STORAGE_ROOT/owncloud/owncloud.db" ]; then # ownCloud 8.1.1 broke upgrades. It may fail on the first attempt, but # that can be OK. sudo -u www-data php5 /usr/local/lib/owncloud/occ upgrade - if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then + E=$? + if [ $E -ne 0 ] && [ $E -ne 3 ]; then echo "Trying ownCloud upgrade again to work around ownCloud upgrade bug..." sudo -u www-data php5 /usr/local/lib/owncloud/occ upgrade - if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi + E=$? + if [ $E -ne 0 ] && [ $E -ne 3 ]; then exit 1; fi sudo -u www-data php5 /usr/local/lib/owncloud/occ maintenance:mode --off echo "...which seemed to work." fi @@ -161,40 +165,40 @@ owncloud_hash=e2b4a4bebd4fac14feae1e6e8997682f73fa8b50 if [ ! -d /usr/local/lib/owncloud/ ] \ || ! grep -q $owncloud_ver /usr/local/lib/owncloud/version.php; then - # Stop php-fpm if running. If theyre not running (which happens on a previously failed install), dont bail. + # Stop php-fpm if running. If they're not running (which happens on a previously failed install), don't bail. service php7.0-fpm stop &> /dev/null || /bin/true service php5-fpm stop &> /dev/null || /bin/true # Backup the existing ownCloud/Nextcloud. # Create a backup directory to store the current installation and database to - BACKUP_DIRECTORY=$STORAGE_ROOT/owncloud-backup/`date +"%Y-%m-%d-%T"` + BACKUP_DIRECTORY=$STORAGE_ROOT/owncloud-backup/$(date +"%Y-%m-%d-%T") mkdir -p "$BACKUP_DIRECTORY" if [ -d /usr/local/lib/owncloud/ ]; then echo "upgrading ownCloud/Nextcloud to $owncloud_flavor $owncloud_ver (backing up existing installation, configuration and database to directory to $BACKUP_DIRECTORY..." cp -r /usr/local/lib/owncloud "$BACKUP_DIRECTORY/owncloud-install" fi if [ -e /home/user-data/owncloud/owncloud.db ]; then - cp /home/user-data/owncloud/owncloud.db $BACKUP_DIRECTORY + cp /home/user-data/owncloud/owncloud.db "$BACKUP_DIRECTORY" fi if [ -e /home/user-data/owncloud/config.php ]; then - cp /home/user-data/owncloud/config.php $BACKUP_DIRECTORY + cp /home/user-data/owncloud/config.php "$BACKUP_DIRECTORY" fi - # We only need to check if we do upgrades when owncloud/Nextcloud was previously installed + # We only need to check if we do upgrades when ownCloud/Nextcloud was previously installed if [ -e /usr/local/lib/owncloud/version.php ]; then if grep -q "OC_VersionString = '8\.1\.[0-9]" /usr/local/lib/owncloud/version.php; then echo "We are running 8.1.x, upgrading to 8.2.11 first" InstallOwncloud 8.2.11 e4794938fc2f15a095018ba9d6ee18b53f6f299c fi - # If we are upgrading from 8.2.x we should go to 9.0 first. Owncloud doesn't support skipping minor versions + # If we are upgrading from 8.2.x we should go to 9.0 first. ownCloud doesn't support skipping minor versions if grep -q "OC_VersionString = '8\.2\.[0-9]" /usr/local/lib/owncloud/version.php; then echo "We are running version 8.2.x, upgrading to 9.0.11 first" # We need to disable memcached. The upgrade and install fails # with memcached CONFIG_TEMP=$(/bin/mktemp) - php < $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php; + php < "$CONFIG_TEMP" && mv "$CONFIG_TEMP" "$STORAGE_ROOT/owncloud/config.php"; EOF - chown www-data.www-data $STORAGE_ROOT/owncloud/config.php + chown www-data.www-data "$STORAGE_ROOT/owncloud/config.php" - # We can now install owncloud 9.0.11 + # We can now install ownCloud 9.0.11 InstallOwncloud 9.0.11 fc8bad8a62179089bc58c406b28997fb0329337b - # The owncloud 9 migration doesn't migrate calendars and contacts + # The ownCloud 9 migration doesn't migrate calendars and contacts # The option to migrate these are removed in 9.1 # So the migrations should be done when we have 9.0 installed sudo -u www-data php5 /usr/local/lib/owncloud/occ dav:migrate-addressbooks - # The following migration has to be done for each owncloud user - for directory in $STORAGE_ROOT/owncloud/*@*/ ; do + # The following migration has to be done for each ownCloud user + for directory in "$STORAGE_ROOT"/owncloud/*@*/ ; do username=$(basename "${directory}") - sudo -u www-data php5 /usr/local/lib/owncloud/occ dav:migrate-calendar $username + sudo -u www-data php5 /usr/local/lib/owncloud/occ dav:migrate-calendar "$username" done sudo -u www-data php5 /usr/local/lib/owncloud/occ dav:sync-birthday-calendar fi @@ -260,13 +264,13 @@ fi # Setup Nextcloud if the Nextcloud database does not yet exist. Running setup when # the database does exist wipes the database and user data. -if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then +if [ ! -f "$STORAGE_ROOT/owncloud/owncloud.db" ]; then # Create user data directory - mkdir -p $STORAGE_ROOT/owncloud + mkdir -p "$STORAGE_ROOT/owncloud" # Create an initial configuration file. - instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1) - cat > $STORAGE_ROOT/owncloud/config.php < "$STORAGE_ROOT/owncloud/config.php" < '$STORAGE_ROOT/owncloud', @@ -317,7 +321,7 @@ EOF EOF # Set permissions - chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud + chown -R www-data.www-data "$STORAGE_ROOT/owncloud" /usr/local/lib/owncloud # Execute Nextcloud's setup step, which creates the Nextcloud sqlite database. # It also wipes it if it exists. And it updates config.php with database @@ -330,15 +334,15 @@ fi # so set it here. It also can change if the box's PRIMARY_HOSTNAME changes, so # this will make sure it has the right value. # * Some settings weren't included in previous versions of Mail-in-a-Box. -# * We need to set the timezone to the system timezone to allow fail2ban to ban +# * We need to set the timezone to the system timezone to allow Fail2Ban to ban # users within the proper timeframe -# * We need to set the logdateformat to something that will work correctly with fail2ban +# * We need to set the logdateformat to something that will work correctly with Fail2Ban # * mail_domain' needs to be set every time we run the setup. Making sure we are setting # the correct domain name if the domain is being change from the previous setup. # Use PHP to read the settings file, modify it, and write out the new settings array. TIMEZONE=$(cat /etc/timezone) CONFIG_TEMP=$(/bin/mktemp) -php < $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php; +php < "$CONFIG_TEMP" && mv "$CONFIG_TEMP" "$STORAGE_ROOT/owncloud/config.php"; EOF -chown www-data.www-data $STORAGE_ROOT/owncloud/config.php +chown www-data.www-data "$STORAGE_ROOT/owncloud/config.php" # Enable/disable apps. Note that this must be done after the Nextcloud setup. # The firstrunwizard gave Josh all sorts of problems, so disabling that. @@ -373,7 +377,8 @@ hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:enable # the first upgrade at the top won't work because apps may be disabled during upgrade? # Check for success (0=ok, 3=no upgrade needed). sudo -u www-data php /usr/local/lib/owncloud/occ upgrade -if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi +E=$? +if [ $E -ne 0 ] && [ $E -ne 3 ]; then exit 1; fi # Set PHP FPM values to support large file uploads # (semicolon is the comment character in this file, hashes produce deprecation warnings) diff --git a/setup/preflight.sh b/setup/preflight.sh index 4be2ec41..c961e57e 100644 --- a/setup/preflight.sh +++ b/setup/preflight.sh @@ -1,20 +1,38 @@ # Are we running as root? if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root. Please re-run like this:" - echo - echo "sudo $0" - echo - exit + echo "This script must be run as root. Please re-run like this:" >&2 + echo >&2 + echo "sudo $0" >&2 + echo >&2 + exit 1 fi +# Check if on Linux +if ! echo "$OSTYPE" | grep -iq "linux"; then + echo "Error: This script must be run on Linux." >&2 + exit 1 +fi + +. /etc/os-release + # Check that we are running on Ubuntu 14.04 LTS (or 14.04.xx). -if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' `" != "Ubuntu 14.04 LTS" ]; then - echo "Mail-in-a-Box only supports being installed on Ubuntu 14.04, sorry. You are running:" - echo - lsb_release -d | sed 's/.*:\s*//' - echo - echo "We can't write scripts that run on every possible setup, sorry." - exit +if ! echo "$ID" | grep -iq "ubuntu" || ! echo "$VERSION_ID" | grep -iq "14.04"; then + if echo "$ID" | grep -iq "ubuntu" && echo "$VERSION_ID" | grep -iq "18.04"; then + echo "Ubuntu 18.04 is not yet fully supported, but is available for testing at: https://github.com/mail-in-a-box/mailinabox/tree/ubuntu_bionic" >&2 + exit 1 + else + echo "Mail-in-a-Box only supports being installed on Ubuntu 14.04, sorry. You are running:" >&2 + echo >&2 + echo "${PRETTY_NAME:-$ID-$VERSION_ID}" >&2 + echo >&2 + echo "We can't write scripts that run on every possible setup, sorry." >&2 + exit 1 + fi +fi + +# Check for the Windows Subsystem for Linux (WSL) +if uname -r | grep -iq "microsoft"; then + echo "Warning: The Windows Subsystem for Linux (WSL) is not yet fully supported by this script." fi # Check that we have enough memory. @@ -25,32 +43,38 @@ fi # We will display a warning if the memory is below 768 MB which is 750000 kibibytes # # Skip the check if we appear to be running inside of Vagrant, because that's really just for testing. -TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}') -if [ $TOTAL_PHYSICAL_MEM -lt 500000 ]; then -if [ ! -d /vagrant ]; then - TOTAL_PHYSICAL_MEM=$(expr \( \( $TOTAL_PHYSICAL_MEM \* 1024 \) / 1000 \) / 1000) - echo "Your Mail-in-a-Box needs more memory (RAM) to function properly." - echo "Please provision a machine with at least 512 MB, 1 GB recommended." - echo "This machine has $TOTAL_PHYSICAL_MEM MB memory." - exit +TOTAL_PHYSICAL_MEM=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo) +TOTAL_SWAP=$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo) +if [ "$TOTAL_PHYSICAL_MEM" -lt 500000 ]; then + if [ ! -d /vagrant ]; then + echo "Your Mail-in-a-Box needs more memory (RAM) to function properly." >&2 + echo "Please provision a machine with at least 512 MB, 1 GB (1024 MB) recommended." >&2 + echo "This machine has $(printf "%'d" $((((TOTAL_PHYSICAL_MEM * 1024) / 1000) / 1000))) MB ($(printf "%'d" $((TOTAL_PHYSICAL_MEM / 1024))) MiB) memory." + exit 1 + fi fi -fi -if [ $TOTAL_PHYSICAL_MEM -lt 750000 ]; then +if [ "$TOTAL_PHYSICAL_MEM" -lt 750000 ]; then echo "WARNING: Your Mail-in-a-Box has less than 768 MB of memory." echo " It might run unreliably when under heavy load." fi +# Check connectivity +if ! ping -q -c 3 mailinabox.email > /dev/null 2>&1; then + echo "Error: Could not reach mailinabox.email, please check your internet connection and run this script again." >&2 + exit 1 +fi + # Check that tempfs is mounted with exec MOUNTED_TMP_AS_NO_EXEC=$(grep "/tmp.*noexec" /proc/mounts) if [ -n "$MOUNTED_TMP_AS_NO_EXEC" ]; then - echo "Mail-in-a-Box has to have exec rights on /tmp, please mount /tmp with exec" - exit + echo "Mail-in-a-Box has to have exec rights on /tmp, please mount /tmp with exec" >&2 + exit 1 fi # Check that no .wgetrc exists if [ -e ~/.wgetrc ]; then - echo "Mail-in-a-Box expects no overrides to wget defaults, ~/.wgetrc exists" - exit + echo "Mail-in-a-Box expects no overrides to wget defaults, ~/.wgetrc exists" >&2 + exit 1 fi # Check that we are running on x86_64 or i686, any other architecture is unsupported and @@ -58,11 +82,11 @@ fi # # Set ARM=1 to ignore this check if you have built the packages yourself. If you do this # you are on your own! -ARCHITECTURE=$(uname -m) -if [ "$ARCHITECTURE" != "x86_64" ] && [ "$ARCHITECTURE" != "i686" ]; then -if [ -z "$ARM" ]; then - echo "Mail-in-a-Box only supports x86_64 or i686 and will not work on any other architecture, like ARM." - echo "Your architecture is $ARCHITECTURE" - exit -fi +ARCHITECTURE=$(getconf LONG_BIT) +if [ "$HOSTTYPE" != "x86_64" ] && [ "$HOSTTYPE" != "i686" ]; then + if [ -z "$ARM" ]; then + echo "Mail-in-a-Box only supports x86_64 or i686 and will not work on any other architecture, like ARM." >&2 + echo "Your architecture is $HOSTTYPE ($ARCHITECTURE-bit)" + exit 1 + fi fi diff --git a/setup/questions.sh b/setup/questions.sh index 3d227d81..00a7a28a 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -7,8 +7,8 @@ if [ -z "$NONINTERACTIVE" ]; then # # Also install dependencies needed to validate the email address. if [ ! -f /usr/bin/dialog ] || [ ! -f /usr/bin/python3 ] || [ ! -f /usr/bin/pip3 ]; then - echo Installing packages needed for setup... - apt-get -q -q update + echo "Installing packages needed for setup..." + hide_output apt-get update apt_get_quiet install dialog python3 python3-pip || exit 1 fi @@ -31,7 +31,7 @@ if [ -z "$PRIMARY_HOSTNAME" ]; then # domain the user possibly wants to use is example.com then. # We strip the string "box." from the hostname to get the mail # domain. If the hostname differs, nothing happens here. - DEFAULT_DOMAIN_GUESS=$(echo $(get_default_hostname) | sed -e 's/^box\.//') + DEFAULT_DOMAIN_GUESS=$(get_default_hostname | sed -e 's/^box\.//') # This is the first run. Ask the user for his email address so we can # provide the best default for the box's hostname. @@ -49,23 +49,23 @@ you really want. if [ -z "$EMAIL_ADDR" ]; then # user hit ESC/cancel - exit + exit 1 fi while ! python3 management/mailconfig.py validate-email "$EMAIL_ADDR" do input_box "Your Email Address" \ "That's not a valid email address.\n\nWhat email address are you setting this box up to manage?" \ - $EMAIL_ADDR \ + "$EMAIL_ADDR" \ EMAIL_ADDR if [ -z "$EMAIL_ADDR" ]; then # user hit ESC/cancel - exit + exit 1 fi done # Take the part after the @-sign as the user's domain name, and add # 'box.' to the beginning to create a default hostname for this machine. - DEFAULT_PRIMARY_HOSTNAME=box.$(echo $EMAIL_ADDR | sed 's/.*@//') + DEFAULT_PRIMARY_HOSTNAME=box.$(echo "$EMAIL_ADDR" | sed 's/.*@//') fi input_box "Hostname" \ @@ -74,12 +74,19 @@ you really want. address, so we're suggesting $DEFAULT_PRIMARY_HOSTNAME. \n\nYou can change it, but we recommend you don't. \n\nHostname:" \ - $DEFAULT_PRIMARY_HOSTNAME \ + "$DEFAULT_PRIMARY_HOSTNAME" \ PRIMARY_HOSTNAME + RE='^.+\.localdomain$' + RE1='^.{4,253}$' + RE2='^([[:alnum:]][[:alnum:]\-]{0,61}[[:alnum:]]\.)+[a-zA-Z]{2,63}$' if [ -z "$PRIMARY_HOSTNAME" ]; then # user hit ESC/cancel - exit + exit 1 + elif [[ $PRIMARY_HOSTNAME =~ $RE ]]; then + echo "Warning: Hostname cannot be *.localdomain." + elif ! [[ $PRIMARY_HOSTNAME =~ $RE1 && $PRIMARY_HOSTNAME =~ $RE2 ]]; then + echo "Warning: Hostname is not a valid fully qualified domain name (FQDN)." fi fi @@ -92,7 +99,7 @@ if [ -z "$PUBLIC_IP" ]; then # On the first run, if we got an answer from the Internet then don't # ask the user. - if [[ -z "$DEFAULT_PUBLIC_IP" && ! -z "$GUESSED_IP" ]]; then + if [[ -z "$DEFAULT_PUBLIC_IP" && -n "$GUESSED_IP" ]]; then PUBLIC_IP=$GUESSED_IP # Otherwise on the first run at least provide a default. @@ -109,12 +116,12 @@ if [ -z "$PUBLIC_IP" ]; then input_box "Public IP Address" \ "Enter the public IP address of this machine, as given to you by your ISP. \n\nPublic IP address:" \ - $DEFAULT_PUBLIC_IP \ + "$DEFAULT_PUBLIC_IP" \ PUBLIC_IP if [ -z "$PUBLIC_IP" ]; then # user hit ESC/cancel - exit + exit 1 fi fi fi @@ -125,7 +132,7 @@ if [ -z "$PUBLIC_IPV6" ]; then # Ask the Internet. GUESSED_IP=$(get_publicip_from_web_service 6) MATCHED=0 - if [[ -z "$DEFAULT_PUBLIC_IPV6" && ! -z "$GUESSED_IP" ]]; then + if [[ -z "$DEFAULT_PUBLIC_IPV6" && -n "$GUESSED_IP" ]]; then PUBLIC_IPV6=$GUESSED_IP elif [[ "$DEFAULT_PUBLIC_IPV6" == "$GUESSED_IP" ]]; then # No IPv6 entered and machine seems to have none, or what @@ -141,12 +148,12 @@ if [ -z "$PUBLIC_IPV6" ]; then "Enter the public IPv6 address of this machine, as given to you by your ISP. \n\nLeave blank if the machine does not have an IPv6 address. \n\nPublic IPv6 address:" \ - $DEFAULT_PUBLIC_IPV6 \ + "$DEFAULT_PUBLIC_IPV6" \ PUBLIC_IPV6 - if [ ! $PUBLIC_IPV6_EXITCODE ]; then + if [ ! "$PUBLIC_IPV6_EXITCODE" ]; then # user hit ESC/cancel - exit + exit 1 fi fi fi @@ -162,13 +169,13 @@ if [ -z "$PRIVATE_IPV6" ]; then fi if [[ -z "$PRIVATE_IP" && -z "$PRIVATE_IPV6" ]]; then echo - echo "I could not determine the IP or IPv6 address of the network inteface" + echo "I could not determine the IP or IPv6 address of the network interface" echo "for connecting to the Internet. Setup must stop." echo hostname -I route echo - exit + exit 1 fi # Automatic configuration, e.g. as used in our Vagrant configuration. @@ -194,19 +201,50 @@ if [ -z "$STORAGE_ROOT" ]; then fi # Show the configuration, since the user may have not entered it manually. -echo -echo "Primary Hostname: $PRIMARY_HOSTNAME" -echo "Public IP Address: $PUBLIC_IP" -if [ ! -z "$PUBLIC_IPV6" ]; then - echo "Public IPv6 Address: $PUBLIC_IPV6" +echo -e "\nLinux Distribution:\t\t${PRETTY_NAME:-$ID-$VERSION_ID}" +CPU=( $(sed -n 's/^model name[[:space:]]*: *//p' /proc/cpuinfo | uniq) ) +if [ -n "$CPU" ]; then + echo -e "Processor (CPU):\t\t${CPU[*]}" fi -if [ "$PRIVATE_IP" != "$PUBLIC_IP" ]; then - echo "Private IP Address: $PRIVATE_IP" +CPU_CORES=$(nproc --all) +echo -e "CPU Cores:\t\t\t$CPU_CORES" +echo -e "Architecture:\t\t\t$HOSTTYPE ($ARCHITECTURE-bit)" +echo -e "Total memory (RAM):\t\t$(printf "%'d" $((TOTAL_PHYSICAL_MEM / 1024))) MiB ($(printf "%'d" $((((TOTAL_PHYSICAL_MEM * 1024) / 1000) / 1000))) MB)" +echo -e "Total swap space:\t\t$(printf "%'d" $((TOTAL_SWAP / 1024))) MiB ($(printf "%'d" $((((TOTAL_SWAP * 1024) / 1000) / 1000))) MB)" +if command -v lspci >/dev/null; then + GPU=( $(lspci 2>/dev/null | grep -i 'vga\|3d\|2d' | sed -n 's/^.*: //p') ) fi -if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then - echo "Private IPv6 Address: $PRIVATE_IPV6" +if [ -n "$GPU" ]; then + echo -e "Graphics Processor (GPU):\t${GPU[*]}" fi -if [ -f /usr/bin/git ] && [ -d .git ]; then - echo "Mail-in-a-Box Version: " $(git describe) +echo -e "Computer name:\t\t\t$HOSTNAME" +echo -e "Primary Hostname:\t\t$PRIMARY_HOSTNAME" +if [ -n "$PUBLIC_IPV6" ]; then + echo -e "Public IPv4 Address:\t\t$PUBLIC_IP" + echo -e "Public IPv6 Address:\t\t$PUBLIC_IPV6" +else + echo -e "Public IP Address:\t\t$PUBLIC_IP" +fi +if [ -n "$PRIVATE_IPV6" ]; then + if [ "$PRIVATE_IP" != "$PUBLIC_IP" ]; then + echo -e "Private IPv4 Address:\t\t$PRIVATE_IP" + fi + if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then + echo -e "Private IPv6 Address:\t\t$PRIVATE_IPV6" + fi +else + if [ "$PRIVATE_IP" != "$PUBLIC_IP" ]; then + echo -e "Private IP Address:\t\t$PRIVATE_IP" + fi +fi +TIME_ZONE=$(timedatectl 2>/dev/null | grep -i 'time zone\|timezone' | sed -n 's/^.*: //p') +echo -e "Time zone:\t\t\t$TIME_ZONE\n" +if command -v systemd-detect-virt >/dev/null && CONTAINER=$(systemd-detect-virt -c); then + echo -e "Virtualization container:\t$CONTAINER\n" +fi +if command -v systemd-detect-virt >/dev/null && VM=$(systemd-detect-virt -v); then + echo -e "Virtual Machine (VM) hypervisor:$VM\n" +fi +if command -v git >/dev/null && [ -d .git ]; then + echo -e "Mail-in-a-Box Version:\t\t$(git describe)\n" fi -echo diff --git a/setup/spamassassin.sh b/setup/spamassassin.sh index 148cfd0b..ce2b284b 100755 --- a/setup/spamassassin.sh +++ b/setup/spamassassin.sh @@ -1,9 +1,9 @@ #!/bin/bash -# Spam filtering with spamassassin via spampd +# Spam filtering with SpamAssassin via spampd # ------------------------------------------- # -# spampd sits between postfix and dovecot. It takes mail from postfix -# over the LMTP protocol, runs spamassassin on it, and then passes the +# spampd sits between Postfix and Dovecot. It takes mail from Postfix +# over the LMTP protocol, runs SpamAssassin on it, and then passes the # message over LMTP to dovecot for local delivery. # # In order to move spam automatically into the Spam folder we use the dovecot sieve @@ -16,13 +16,13 @@ source setup/functions.sh # load our functions # ---------------------------------------- # Install packages. -# libmail-dkim-perl is needed to make the spamassassin DKIM module work. +# libmail-dkim-perl is needed to make the SpamAssassin DKIM module work. # For more information see Debian Bug #689414: # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=689414 echo "Installing SpamAssassin..." apt_install spampd razor pyzor dovecot-antispam libmail-dkim-perl -# Allow spamassassin to download new rules. +# Allow SpamAssassin to download new rules. tools/editconf.py /etc/default/spamassassin \ CRON=1 @@ -41,26 +41,26 @@ echo "public.pyzor.org:24441" > /etc/spamassassin/pyzor/servers # check with: pyzor --homedir /etc/mail/spamassassin/pyzor ping # Configure spampd: -# * Pass messages on to docevot on port 10026. This is actually the default setting but we don't +# * Pass messages on to Dovecot on port 10026. This is actually the default setting but we don't # want to lose track of it. (We've configured Dovecot to listen on this port elsewhere.) # * Increase the maximum message size of scanned messages from the default of 64KB to 500KB, which -# is Spamassassin (spamc)'s own default. Specified in KBytes. +# is SpamAssassin (spamc)'s own default. Specified in KBytes. # * Disable localmode so Pyzor, DKIM and DNS checks can be used. tools/editconf.py /etc/default/spampd \ DESTPORT=10026 \ ADDOPTS="\"--maxsize=2000\"" \ LOCALONLY=0 -# Spamassassin normally wraps spam as an attachment inside a fresh +# SpamAssassin normally wraps spam as an attachment inside a fresh # email with a report about the message. This also protects the user -# from accidentally openening a message with embedded malware. +# from accidentally opening a message with embedded malware. # # It's nice to see what rules caused the message to be marked as spam, # but it's also annoying to get to the original message when it is an # attachment, modern mail clients are safer now and don't load remote # content or execute scripts, and it is probably confusing to most users. # -# Tell Spamassassin not to modify the original message except for adding +# Tell SpamAssassin not to modify the original message except for adding # the X-Spam-Status & X-Spam-Score mail headers and related headers. tools/editconf.py /etc/spamassassin/local.cf -s \ report_safe=0 \ @@ -70,26 +70,26 @@ tools/editconf.py /etc/spamassassin/local.cf -s \ # Bayesean learning # ----------------- # -# Spamassassin can learn from mail marked as spam or ham, but it needs to be +# SpamAssassin can learn from mail marked as spam or ham, but it needs to be # configured. We'll store the learning data in our storage area. # # These files must be: # -# * Writable by sa-learn-pipe script below, which run as the 'mail' user, for manual tagging of mail as spam/ham. +# * Writeable by sa-learn-pipe script below, which run as the 'mail' user, for manual tagging of mail as spam/ham. # * Readable by the spampd process ('spampd' user) during mail filtering. -# * Writable by the debian-spamd user, which runs /etc/cron.daily/spamassassin. +# * Writeable by the debian-spamd user, which runs /etc/cron.daily/spamassassin. # # We'll have these files owned by spampd and grant access to the other two processes. # -# Spamassassin will change the access rights back to the defaults, so we must also configure +# SpamAssassin will change the access rights back to the defaults, so we must also configure # the filemode in the config file. tools/editconf.py /etc/spamassassin/local.cf -s \ - bayes_path=$STORAGE_ROOT/mail/spamassassin/bayes \ + bayes_path="$STORAGE_ROOT/mail/spamassassin/bayes" \ bayes_file_mode=0666 -mkdir -p $STORAGE_ROOT/mail/spamassassin -chown -R spampd:spampd $STORAGE_ROOT/mail/spamassassin +mkdir -p "$STORAGE_ROOT/mail/spamassassin" +chown -R spampd:spampd "$STORAGE_ROOT/mail/spamassassin" # To mark mail as spam or ham, just drag it in or out of the Spam folder. We'll # use the Dovecot antispam plugin to detect the message move operation and execute @@ -134,8 +134,8 @@ chmod a+x /usr/local/bin/sa-learn-pipe.sh # Create empty bayes training data (if it doesn't exist). Once the files exist, # ensure they are group-writable so that the Dovecot process has access. sudo -u spampd /usr/bin/sa-learn --sync 2>/dev/null -chmod -R 660 $STORAGE_ROOT/mail/spamassassin -chmod 770 $STORAGE_ROOT/mail/spamassassin +chmod -R 660 "$STORAGE_ROOT/mail/spamassassin" +chmod 770 "$STORAGE_ROOT/mail/spamassassin" # Initial training? # sa-learn --ham storage/mail/mailboxes/*/*/cur/ diff --git a/setup/ssl.sh b/setup/ssl.sh index 61b0b9e5..806bf603 100755 --- a/setup/ssl.sh +++ b/setup/ssl.sh @@ -26,9 +26,9 @@ source /etc/mailinabox.conf # load global vars # Show a status line if we are going to take any action in this file. if [ ! -f /usr/bin/openssl ] \ - || [ ! -f $STORAGE_ROOT/ssl/ssl_private_key.pem ] \ - || [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ] \ - || [ ! -f $STORAGE_ROOT/ssl/dh2048.pem ]; then + || [ ! -f "$STORAGE_ROOT/ssl/ssl_private_key.pem" ] \ + || [ ! -f "$STORAGE_ROOT/ssl/ssl_certificate.pem" ] \ + || [ ! -f "$STORAGE_ROOT/ssl/dh2048.pem" ]; then echo "Creating initial SSL certificate and perfect forward secrecy Diffie-Hellman parameters..." fi @@ -38,7 +38,7 @@ apt_install openssl # Create a directory to store TLS-related things like "SSL" certificates. -mkdir -p $STORAGE_ROOT/ssl +mkdir -p "$STORAGE_ROOT/ssl" # Generate a new private key. # @@ -60,39 +60,39 @@ mkdir -p $STORAGE_ROOT/ssl # # Since we properly seed /dev/urandom in system.sh we should be fine, but I leave # in the rest of the notes in case that ever changes. -if [ ! -f $STORAGE_ROOT/ssl/ssl_private_key.pem ]; then +if [ ! -f "$STORAGE_ROOT/ssl/ssl_private_key.pem" ]; then # Set the umask so the key file is never world-readable. (umask 077; hide_output \ - openssl genrsa -out $STORAGE_ROOT/ssl/ssl_private_key.pem 2048) + openssl genrsa -out "$STORAGE_ROOT/ssl/ssl_private_key.pem" 2048) fi # Generate a self-signed SSL certificate because things like nginx, dovecot, # etc. won't even start without some certificate in place, and we need nginx # so we can offer the user a control panel to install a better certificate. -if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then +if [ ! -f "$STORAGE_ROOT/ssl/ssl_certificate.pem" ]; then # Generate a certificate signing request. CSR=/tmp/ssl_cert_sign_req-$$.csr hide_output \ - openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CSR \ + openssl req -new -key "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out $CSR \ -sha256 -subj "/CN=$PRIMARY_HOSTNAME" # Generate the self-signed certificate. CERT=$STORAGE_ROOT/ssl/$PRIMARY_HOSTNAME-selfsigned-$(date --rfc-3339=date | sed s/-//g).pem hide_output \ openssl x509 -req -days 365 \ - -in $CSR -signkey $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CERT + -in $CSR -signkey "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out "$CERT" # Delete the certificate signing request because it has no other purpose. rm -f $CSR # Symlink the certificate into the system certificate path, so system services # can find it. - ln -s $CERT $STORAGE_ROOT/ssl/ssl_certificate.pem + ln -s "$CERT" "$STORAGE_ROOT/ssl/ssl_certificate.pem" fi # Generate some Diffie-Hellman cipher bits. # openssl's default bit length for this is 1024 bits, but we'll create # 2048 bits of bits per the latest recommendations. -if [ ! -f $STORAGE_ROOT/ssl/dh2048.pem ]; then - openssl dhparam -out $STORAGE_ROOT/ssl/dh2048.pem 2048 +if [ ! -f "$STORAGE_ROOT/ssl/dh2048.pem" ]; then + openssl dhparam -out "$STORAGE_ROOT/ssl/dh2048.pem" 2048 fi diff --git a/setup/start.sh b/setup/start.sh index 3dbaed54..92bd3396 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -2,6 +2,11 @@ # This is the entry point for configuring the system. ##################################################### +if [[ "$#" -ne 0 ]]; then + echo "Usage: sudo $0" >&2 + exit 1 +fi + source setup/functions.sh # load our functions # Check system setup: Are we running as root on Ubuntu 14.04 on a @@ -35,7 +40,8 @@ if [ -f /etc/mailinabox.conf ]; then # Load the old .conf file to get existing configuration options loaded # into variables with a DEFAULT_ prefix. - cat /etc/mailinabox.conf | sed s/^/DEFAULT_/ > /tmp/mailinabox.prev.conf + cp /etc/mailinabox.conf /tmp/mailinabox.prev.conf + sed -i 's/^/DEFAULT_/' /tmp/mailinabox.prev.conf source /tmp/mailinabox.prev.conf rm -f /tmp/mailinabox.prev.conf else @@ -46,7 +52,7 @@ fi # in the first dialog prompt, so we should do this before that starts. cat > /usr/local/bin/mailinabox << EOF; #!/bin/bash -cd `pwd` +cd $(pwd) source setup/start.sh EOF chmod +x /usr/local/bin/mailinabox @@ -61,9 +67,9 @@ source setup/questions.sh # Skip on existing installs since we don't want this to block the ability to # upgrade, and these checks are also in the control panel status checks. if [ -z "$DEFAULT_PRIMARY_HOSTNAME" ]; then -if [ -z "$SKIP_NETWORK_CHECKS" ]; then - source setup/network-checks.sh -fi + if [ -z "$SKIP_NETWORK_CHECKS" ]; then + source setup/network-checks.sh + fi fi # Create the STORAGE_USER and STORAGE_ROOT directory if they don't already exist. @@ -71,15 +77,15 @@ fi # migration (schema) number for the files stored there, assume this is a fresh # installation to that directory and write the file to contain the current # migration number for this version of Mail-in-a-Box. -if ! id -u $STORAGE_USER >/dev/null 2>&1; then - useradd -m $STORAGE_USER +if ! id -u "$STORAGE_USER" >/dev/null 2>&1; then + useradd -m "$STORAGE_USER" fi -if [ ! -d $STORAGE_ROOT ]; then - mkdir -p $STORAGE_ROOT +if [ ! -d "$STORAGE_ROOT" ]; then + mkdir -p "$STORAGE_ROOT" fi -if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then - echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox.version - chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version +if [ ! -f "$STORAGE_ROOT/mailinabox.version" ]; then + setup/migrate.py --current > "$STORAGE_ROOT/mailinabox.version" + chown "$STORAGE_USER.$STORAGE_USER" "$STORAGE_ROOT/mailinabox.version" fi @@ -114,7 +120,7 @@ source setup/munin.sh # Wait for the management daemon to start... until nc -z -w 4 127.0.0.1 10222 do - echo Waiting for the Mail-in-a-Box management daemon to start... + echo "Waiting for the Mail-in-a-Box management daemon to start..." sleep 2 done @@ -123,8 +129,8 @@ done tools/dns_update tools/web_update -# Give fail2ban another restart. The log files may not all have been present when -# fail2ban was first configured, but they should exist now. +# Give Fail2Ban another restart. The log files may not all have been present when +# Fail2Ban was first configured, but they should exist now. restart_service fail2ban # If there aren't any mail users yet, create one. @@ -134,41 +140,41 @@ source setup/firstuser.sh # We'd let certbot ask the user interactively, but when this script is # run in the recommended curl-pipe-to-bash method there is no TTY and # certbot will fail if it tries to ask. -if [ ! -d $STORAGE_ROOT/ssl/lets_encrypt/accounts/acme-v02.api.letsencrypt.org/ ]; then -echo -echo "-----------------------------------------------" -echo "Mail-in-a-Box uses Let's Encrypt to provision free SSL/TLS certificates" -echo "to enable HTTPS connections to your box. We're automatically" -echo "agreeing you to their subscriber agreement. See https://letsencrypt.org." -echo -certbot register --register-unsafely-without-email --agree-tos --config-dir $STORAGE_ROOT/ssl/lets_encrypt +if [ ! -d "$STORAGE_ROOT/ssl/lets_encrypt/accounts/acme-v02.api.letsencrypt.org/" ]; then + echo + echo "-----------------------------------------------" + echo "Mail-in-a-Box uses Let's Encrypt to provision free SSL/TLS certificates" + echo "to enable HTTPS connections to your box. We're automatically" + echo "agreeing you to their subscriber agreement. See https://letsencrypt.org." + echo + certbot register -n --register-unsafely-without-email --agree-tos --config-dir "$STORAGE_ROOT/ssl/lets_encrypt" fi # Done. echo echo "-----------------------------------------------" echo -echo Your Mail-in-a-Box is running. +echo "Your Mail-in-a-Box is running." echo -echo Please log in to the control panel for further instructions at: +echo "Please log in to the control panel for further instructions at:" echo if management/status_checks.py --check-primary-hostname; then # Show the nice URL if it appears to be resolving and has a valid certificate. - echo https://$PRIMARY_HOSTNAME/admin + echo "https://$PRIMARY_HOSTNAME/admin" echo echo "If you have a DNS problem put the box's IP address in the URL" echo "(https://$PUBLIC_IP/admin) but then check the TLS fingerprint:" - openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\ + openssl x509 -in "$STORAGE_ROOT/ssl/ssl_certificate.pem" -noout -fingerprint -sha256\ | sed "s/SHA256 Fingerprint=//" else - echo https://$PUBLIC_IP/admin + echo "https://$PUBLIC_IP/admin" echo - echo You will be alerted that the website has an invalid certificate. Check that - echo the certificate fingerprint matches: + echo "You will be alerted that the website has an invalid certificate. Check that" + echo "the certificate fingerprint matches:" echo - openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\ + openssl x509 -in "$STORAGE_ROOT/ssl/ssl_certificate.pem" -noout -fingerprint -sha256\ | sed "s/SHA256 Fingerprint=//" echo - echo Then you can confirm the security exception and continue. + echo "Then you can confirm the security exception and continue." echo fi diff --git a/setup/system.sh b/setup/system.sh index 04728051..e291ca29 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -11,13 +11,12 @@ source setup/functions.sh # load our functions # # First set the hostname in the configuration file, then activate the setting -echo $PRIMARY_HOSTNAME > /etc/hostname -hostname $PRIMARY_HOSTNAME +hostnamectl set-hostname "$PRIMARY_HOSTNAME" # ### Add swap space to the system # If the physical memory of the system is below 2GB it is wise to create a -# swap file. This will make the system more resiliant to memory spikes and +# swap file. This will make the system more resilient to memory spikes and # prevent for instance spam filtering from crashing # We will create a 1G file, this should be a good balance between disk usage @@ -36,7 +35,7 @@ hostname $PRIMARY_HOSTNAME # See https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04 # for reference -SWAP_MOUNTED=$(cat /proc/swaps | tail -n+2) +SWAP_MOUNTED=$(< /proc/swaps tail -n+2) SWAP_IN_FSTAB=$(grep "swap" /etc/fstab) ROOT_IS_BTRFS=$(grep "\/ .*btrfs" /proc/mounts) TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}') @@ -46,14 +45,14 @@ if [ -z "$SWAP_IN_FSTAB" ] && [ ! -e /swapfile ] && [ -z "$ROOT_IS_BTRFS" ] && - [ $TOTAL_PHYSICAL_MEM -lt 1900000 ] && - [ $AVAILABLE_DISK_SPACE -gt 5242880 ] + [ "$TOTAL_PHYSICAL_MEM" -lt 1900000 ] && + [ "$AVAILABLE_DISK_SPACE" -gt 5242880 ] then echo "Adding a swap file to the system..." - # Allocate and activate the swap file. Allocate in 1KB chuncks + # Allocate and activate the swap file. Allocate in 1KB chunks # doing it in one go, could fail on low memory systems - dd if=/dev/zero of=/swapfile bs=1024 count=$[1024*1024] status=none + dd if=/dev/zero of=/swapfile bs=1024 count=$((1024*1024)) status=none if [ -e /swapfile ]; then chmod 600 /swapfile hide_output mkswap /swapfile @@ -95,9 +94,9 @@ hide_output add-apt-repository -y ppa:certbot/certbot # of things from Ubuntu, as well as the directory of packages provide by the # PPAs so we can install those packages later. -echo Updating system packages... +echo "Updating system packages..." hide_output apt-get update -apt_get_quiet upgrade +apt_get_quiet dist-upgrade # Old kernels pile up over time and take up a lot of disk space, and because of Mail-in-a-Box # changes there may be other packages that are no longer needed. Clear out anything apt knows @@ -117,12 +116,12 @@ apt_get_quiet autoremove # * ntp: keeps the system time correct # * fail2ban: scans log files for repeated failed login attempts and blocks the remote IP at the firewall # * netcat-openbsd: `nc` command line networking tool -# * git: we install some things directly from github +# * git: we install some things directly from GitHub # * sudo: allows privileged users to execute commands as root without being root # * coreutils: includes `nproc` tool to report number of processors, mktemp # * bc: allows us to do math to compute sane defaults -echo Installing system packages... +echo "Installing system packages..." apt_install python3 python3-dev python3-pip \ netcat-openbsd wget curl git sudo coreutils bc \ haveged pollinate unzip \ @@ -157,13 +156,13 @@ fi # things (i.e. late at night in whatever timezone the user actually lives # in). # -# However, changing the timezone once it is set seems to confuse fail2ban -# and requires restarting fail2ban (done below in the fail2ban +# However, changing the timezone once it is set seems to confuse Fail2Ban +# and requires restarting Fail2Ban (done below in the Fail2Ban # section) and syslog (see #328). There might be other issues, and it's # not likely the user will want to change this, so we only ask on first # setup. if [ -z "$NONINTERACTIVE" ]; then - if [ ! -f /etc/timezone ] || [ ! -z $FIRST_TIME_SETUP ]; then + if [ ! -f /etc/timezone ] || [ -n "$FIRST_TIME_SETUP" ]; then # If the file is missing or this is the user's first time running # Mail-in-a-Box setup, run the interactive timezone configuration # tool. @@ -226,7 +225,7 @@ fi # hardware entropy to get going, by drawing from /dev/random. haveged makes this # less likely to stall for very long. -echo Initializing system random number generator... +echo "Initializing system random number generator..." dd if=/dev/random of=/dev/urandom bs=1 count=32 2> /dev/null # This is supposedly sufficient. But because we're not sure if hardware entropy @@ -237,7 +236,7 @@ pollinate -q -r # Between these two, we really ought to be all set. -# We need an ssh key to store backups via rsync, if it doesn't exist create one +# We need an SSH key to store backups via rsync, if it doesn't exist create one if [ ! -f /root/.ssh/id_rsa_miab ]; then echo 'Creating SSH key for backup…' ssh-keygen -t rsa -b 2048 -a 100 -f /root/.ssh/id_rsa_miab -N '' -q @@ -270,11 +269,11 @@ if [ -z "$DISABLE_FIREWALL" ]; then # settings, find the port it is supposedly running on, and open that port #NODOC # too. #NODOC SSH_PORT=$(sshd -T 2>/dev/null | grep "^port " | sed "s/port //") #NODOC - if [ ! -z "$SSH_PORT" ]; then + if [ -n "$SSH_PORT" ]; then if [ "$SSH_PORT" != "22" ]; then - echo Opening alternate SSH port $SSH_PORT. #NODOC - ufw_allow $SSH_PORT #NODOC + echo "Opening alternate SSH port $SSH_PORT." #NODOC + ufw_allow "$SSH_PORT" #NODOC fi fi @@ -328,15 +327,14 @@ restart_service resolvconf # Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix, ssh, etc. rm -f /etc/fail2ban/jail.local # we used to use this file but don't anymore -cat conf/fail2ban/jails.conf \ - | sed "s/PUBLIC_IP/$PUBLIC_IP/g" \ +< conf/fail2ban/jails.conf sed "s/PUBLIC_IP/$PUBLIC_IP/g" \ | sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \ > /etc/fail2ban/jail.d/mailinabox.conf cp -f conf/fail2ban/filter.d/* /etc/fail2ban/filter.d/ # On first installation, the log files that the jails look at don't all exist. -# e.g., The roundcube error log isn't normally created until someone logs into -# Roundcube for the first time. This causes fail2ban to fail to start. Later -# scripts will ensure the files exist and then fail2ban is given another +# e.g., The Roundcube error log isn't normally created until someone logs into +# Roundcube for the first time. This causes Fail2Ban to fail to start. Later +# scripts will ensure the files exist and then Fail2Ban is given another # restart at the very end of setup. restart_service fail2ban diff --git a/setup/web.sh b/setup/web.sh index 021a9b7f..18cc5ebf 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -8,7 +8,7 @@ source /etc/mailinabox.conf # load global vars # Some Ubuntu images start off with Apache. Remove it since we # will use nginx. Use autoremove to remove any Apache depenencies. if [ -f /usr/sbin/apache2 ]; then - echo Removing apache... + echo "Removing Apache..." hide_output apt-get -y purge apache2 apache2-* hide_output apt-get -y --purge autoremove fi @@ -17,7 +17,7 @@ fi # # Turn off nginx's default website. -echo "Installing Nginx (web server)..." +echo "Installing nginx (web server)..." apt_install nginx php7.0-cli php7.0-fpm @@ -67,8 +67,7 @@ tools/editconf.py /etc/php/7.0/fpm/pool.d/www.conf -c ';' \ # nginx configuration at /mailinabox-mobileconfig. mkdir -p /var/lib/mailinabox chmod a+rx /var/lib/mailinabox -cat conf/ios-profile.xml \ - | sed "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" \ +< conf/ios-profile.xml sed "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" \ | sed "s/UUID1/$(cat /proc/sys/kernel/random/uuid)/" \ | sed "s/UUID2/$(cat /proc/sys/kernel/random/uuid)/" \ | sed "s/UUID3/$(cat /proc/sys/kernel/random/uuid)/" \ @@ -81,18 +80,17 @@ chmod a+r /var/lib/mailinabox/mobileconfig.xml # The format of the file is documented at: # https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat # and https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration/FileFormat/HowTo. -cat conf/mozilla-autoconfig.xml \ - | sed "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" \ +< conf/mozilla-autoconfig.xml sed "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" \ > /var/lib/mailinabox/mozilla-autoconfig.xml chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml # make a default homepage -if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_ROOT/www/default; fi # migration #NODOC -mkdir -p $STORAGE_ROOT/www/default -if [ ! -f $STORAGE_ROOT/www/default/index.html ]; then - cp conf/www_default.html $STORAGE_ROOT/www/default/index.html +if [ -d "$STORAGE_ROOT/www/static" ]; then mv "$STORAGE_ROOT/www/static" "$STORAGE_ROOT/www/default"; fi # migration #NODOC +mkdir -p "$STORAGE_ROOT/www/default" +if [ ! -f "$STORAGE_ROOT/www/default/index.html" ]; then + cp conf/www_default.html "$STORAGE_ROOT/www/default/index.html" fi -chown -R $STORAGE_USER $STORAGE_ROOT/www +chown -R "$STORAGE_USER" "$STORAGE_ROOT/www" # We previously installed a custom init script to start the PHP FastCGI daemon. #NODOC # Remove it now that we're using php5-fpm. #NODOC diff --git a/setup/webmail.sh b/setup/webmail.sh index 38aee942..ad5daa5c 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -15,8 +15,8 @@ source /etc/mailinabox.conf # load global vars # # 3. It's packaged incorrectly --- it seems to be missing a directory of files. # -# So we'll use apt-get to manually install the dependencies of roundcube that we know we need, -# and then we'll manually install roundcube from source. +# So we'll use apt-get to manually install the dependencies of Roundcube that we know we need, +# and then we'll manually install Roundcube from source. # These dependencies are from `apt-cache showpkg roundcube-core`. echo "Installing Roundcube (webmail)..." @@ -53,7 +53,7 @@ needs_update=0 #NODOC if [ ! -f /usr/local/lib/roundcubemail/version ]; then # not installed yet #NODOC needs_update=1 #NODOC -elif [[ "$UPDATE_KEY" != `cat /usr/local/lib/roundcubemail/version` ]]; then +elif [[ "$UPDATE_KEY" != $(cat /usr/local/lib/roundcubemail/version) ]]; then # checks if the version is what we want needs_update=1 #NODOC fi @@ -68,10 +68,10 @@ if [ $needs_update == 1 ]; then mv /usr/local/lib/roundcubemail-$VERSION/ $RCM_DIR rm -f /tmp/roundcube.tgz - # install roundcube persistent_login plugin + # install Roundcube persistent_login plugin git_clone https://github.com/mfreiholz/Roundcube-Persistent-Login-Plugin.git $PERSISTENT_LOGIN_VERSION '' ${RCM_PLUGIN_DIR}/persistent_login - # install roundcube html5_notifier plugin + # install Roundcube html5_notifier plugin git_clone https://github.com/kitist/html5_notifier.git $HTML5_NOTIFIER_VERSION '' ${RCM_PLUGIN_DIR}/html5_notifier # download and verify the full release of the carddav plugin @@ -96,7 +96,7 @@ SECRET_KEY=$(dd if=/dev/urandom bs=1 count=18 2>/dev/null | base64 | fold -w 24 # Create a configuration file. # # For security, temp and log files are not stored in the default locations -# which are inside the roundcube sources directory. We put them instead +# which are inside the Roundcube sources directory. We put them instead # in normal places. cat > $RCM_CONFIG < ${RCM_PLUGIN_DIR}/carddav/config.inc.php < EOF -# Create writable directories. -mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube -chown -R www-data.www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube +# Create writeable directories. +mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail "$STORAGE_ROOT/mail/roundcube" +chown -R www-data.www-data /var/log/roundcubemail /var/tmp/roundcubemail "$STORAGE_ROOT/mail/roundcube" -# Ensure the log file monitored by fail2ban exists, or else fail2ban can't start. +# Ensure the log file monitored by Fail2Ban exists, or else Fail2Ban can't start. sudo -u www-data touch /var/log/roundcubemail/errors # Password changing plugin settings @@ -184,10 +184,10 @@ usermod -a -G dovecot www-data # set permissions so that PHP can use users.sqlite # could use dovecot instead of www-data, but not sure it matters -chown root.www-data $STORAGE_ROOT/mail -chmod 775 $STORAGE_ROOT/mail -chown root.www-data $STORAGE_ROOT/mail/users.sqlite -chmod 664 $STORAGE_ROOT/mail/users.sqlite +chown root.www-data "$STORAGE_ROOT/mail" +chmod 775 "$STORAGE_ROOT/mail" +chown root.www-data "$STORAGE_ROOT/mail/users.sqlite" +chmod 664 "$STORAGE_ROOT/mail/users.sqlite" # Fix Carddav permissions: chown -f -R root.www-data ${RCM_PLUGIN_DIR}/carddav @@ -196,8 +196,8 @@ chmod -R 774 ${RCM_PLUGIN_DIR}/carddav # Run Roundcube database migration script (database is created if it does not exist) ${RCM_DIR}/bin/updatedb.sh --dir ${RCM_DIR}/SQL --package roundcube -chown www-data:www-data $STORAGE_ROOT/mail/roundcube/roundcube.sqlite -chmod 664 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite +chown www-data:www-data "$STORAGE_ROOT/mail/roundcube/roundcube.sqlite" +chmod 664 "$STORAGE_ROOT/mail/roundcube/roundcube.sqlite" # Enable PHP modules. phpenmod -v php7.0 mcrypt imap diff --git a/setup/zpush.sh b/setup/zpush.sh index 84ab4556..c43dc5d4 100755 --- a/setup/zpush.sh +++ b/setup/zpush.sh @@ -27,7 +27,7 @@ TARGETHASH=104d44426852429dac8ec2783a4e9ad7752d4682 needs_update=0 #NODOC if [ ! -f /usr/local/lib/z-push/version ]; then needs_update=1 #NODOC -elif [[ $VERSION != `cat /usr/local/lib/z-push/version` ]]; then +elif [[ $VERSION != $(cat /usr/local/lib/z-push/version) ]]; then # checks if the version needs_update=1 #NODOC fi diff --git a/tools/archive_conf_files.sh b/tools/archive_conf_files.sh index 28b6b24f..aa6d0c59 100644 --- a/tools/archive_conf_files.sh +++ b/tools/archive_conf_files.sh @@ -1,9 +1,9 @@ # Use this script to make an archive of the contents of all # of the configuration files we edit with editconf.py. -for fn in `grep -hr editconf.py setup | sed "s/tools\/editconf.py //" | sed "s/ .*//" | sort | uniq`; do - echo ====================================================================== - echo $fn - echo ====================================================================== - cat $fn +for fn in $(grep -hr editconf.py setup | sed "s/tools\/editconf.py //" | sed "s/ .*//" | sort | uniq); do + echo "======================================================================" + echo "$fn" + echo "======================================================================" + cat "$fn" done diff --git a/tools/owncloud-restore.sh b/tools/owncloud-restore.sh index 1b006ca1..ad6c99a3 100755 --- a/tools/owncloud-restore.sh +++ b/tools/owncloud-restore.sh @@ -14,15 +14,15 @@ if [ -z "$1" ]; then echo echo "Available backups:" echo - find $STORAGE_ROOT/owncloud-backup/* -maxdepth 0 -type d + find "$STORAGE_ROOT/owncloud-backup/*" -maxdepth 0 -type d echo echo "Supply the directory that was created during the last installation as the only commandline argument" - exit + exit 1 fi -if [ ! -f $1/config.php ]; then +if [ ! -f "$1/config.php" ]; then echo "This isn't a valid backup location" - exit + exit 1 fi echo "Restoring backup from $1" @@ -37,12 +37,12 @@ cp -r "$1/owncloud-install" /usr/local/lib/owncloud # restore access rights chmod 750 /usr/local/lib/owncloud/{apps,config} -cp "$1/owncloud.db" $STORAGE_ROOT/owncloud/ -cp "$1/config.php" $STORAGE_ROOT/owncloud/ +cp "$1/owncloud.db" "$STORAGE_ROOT/owncloud/" +cp "$1/config.php" "$STORAGE_ROOT/owncloud/" -ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php -chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud -chown www-data.www-data $STORAGE_ROOT/owncloud/config.php +ln -sf "$STORAGE_ROOT/owncloud/config.php" /usr/local/lib/owncloud/config/config.php +chown -f -R www-data.www-data "$STORAGE_ROOT/owncloud /usr/local/lib/owncloud" +chown www-data.www-data "$STORAGE_ROOT/owncloud/config.php" sudo -u www-data php /usr/local/lib/owncloud/occ maintenance:mode --off diff --git a/tools/owncloud-unlockadmin.sh b/tools/owncloud-unlockadmin.sh index 50e3b010..0a5917ed 100755 --- a/tools/owncloud-unlockadmin.sh +++ b/tools/owncloud-unlockadmin.sh @@ -4,20 +4,20 @@ # instance running here. # # Run this at your own risk. This is for testing & experimentation -# purpopses only. After this point you are on your own. +# purposes only. After this point you are on your own. source /etc/mailinabox.conf # load global vars ADMIN=$(./mail.py user admins | head -n 1) test -z "$1" || ADMIN=$1 -echo I am going to unlock admin features for $ADMIN. -echo You can provide another user to unlock as the first argument of this script. +echo "I am going to unlock admin features for $ADMIN." +echo "You can provide another user to unlock as the first argument of this script." echo -echo WARNING: you could break mail-in-a-box when fiddling around with Nextcloud\'s admin interface -echo If in doubt, press CTRL-C to cancel. +echo "WARNING: you could break mail-in-a-box when fiddling around with Nextcloud\'s admin interface" +echo "If in doubt, press CTRL-C to cancel." echo -echo Press enter to continue. +echo "Press enter to continue." read -sudo -u www-data php /usr/local/lib/owncloud/occ group:adduser admin $ADMIN && echo Done. +sudo -u www-data php /usr/local/lib/owncloud/occ group:adduser admin "$ADMIN" && echo "Done."