diff --git a/README.md b/README.md index 05808b4..3599d37 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ In short, it's like this: cd mailinabox sudo setup/start.sh +Then run the post-install checklist command to see what you need to do next: + + sudo management/whats_next.py + +Congratulations! You should now have a working setup. Feel free to login with your mail credentials created earlier in the setup + **Status**: This is a work in progress. It works for what it is, but it is missing such things as quotas, backup/restore, etc. The Goals diff --git a/conf/nginx-primaryonly.conf b/conf/nginx-primaryonly.conf new file mode 100644 index 0000000..d7457ed --- /dev/null +++ b/conf/nginx-primaryonly.conf @@ -0,0 +1,41 @@ + # ownCloud configuration. + rewrite ^/cloud$ /cloud/ redirect; + rewrite ^/cloud/$ /cloud/index.php; + rewrite ^(/cloud/core/doc/[^\/]+/)$ $1/index.html; + location /cloud/ { + alias /usr/local/lib/owncloud/; + location ~ ^/(data|config|\.ht|db_structure\.xml|README) { + deny all; + } + } + location ~ ^(/cloud)(/[^/]+\.php)(/.*)?$ { + # note: ~ has precendence over a regular location block + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /usr/local/lib/owncloud/$2; + fastcgi_param SCRIPT_NAME $1$2; + fastcgi_param PATH_INFO $3; + fastcgi_param MOD_X_ACCEL_REDIRECT_ENABLED on; + fastcgi_read_timeout 630; + fastcgi_pass php-fpm; + error_page 403 /cloud/core/templates/403.php; + error_page 404 /cloud/core/templates/404.php; + client_max_body_size 1G; + fastcgi_buffers 64 4K; + } + location ^~ /cloud/data { + # In order to support MOD_X_ACCEL_REDIRECT_ENABLED, we need to expose + # the data directory but only allow 'internal' redirects within nginx + # so that this is not exposed to the world. + internal; + alias $STORAGE_ROOT/owncloud; + } + location ~ ^/((caldav|carddav|webdav).*)$ { + # Z-Push doesn't like getting a redirect, and a plain rewrite didn't work either. + # Properly proxying like this seems to work fine. + proxy_pass https://$HOSTNAME/cloud/remote.php/$1; + } + rewrite ^/.well-known/host-meta /cloud/public.php?service=host-meta last; + rewrite ^/.well-known/host-meta.json /cloud/public.php?service=host-meta-json last; + rewrite ^/.well-known/carddav /cloud/remote.php/carddav/ redirect; + rewrite ^/.well-known/caldav /cloud/remote.php/caldav/ redirect; + diff --git a/conf/nginx.conf b/conf/nginx.conf index da371b9..4f343c5 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -36,6 +36,7 @@ server { return 403; } location ~ /mail/.*\.php { + # note: ~ has precendence over a regular location block include fastcgi_params; fastcgi_split_path_info ^/mail(/.*)()$; fastcgi_index index.php; @@ -60,8 +61,9 @@ server { # Z-Push (Microsoft Exchange ActiveSync) location /Microsoft-Server-ActiveSync { - include /etc/nginx/fastcgi_params; + include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php; + fastcgi_read_timeout 630; fastcgi_pass php-fpm; } diff --git a/conf/zpush/backend_caldav.php b/conf/zpush/backend_caldav.php new file mode 100644 index 0000000..309a181 --- /dev/null +++ b/conf/zpush/backend_caldav.php @@ -0,0 +1,18 @@ + diff --git a/conf/zpush/backend_carddav.php b/conf/zpush/backend_carddav.php new file mode 100644 index 0000000..f3e8937 --- /dev/null +++ b/conf/zpush/backend_carddav.php @@ -0,0 +1,37 @@ + diff --git a/conf/zpush/backend_combined.php b/conf/zpush/backend_combined.php new file mode 100644 index 0000000..9d5aea2 --- /dev/null +++ b/conf/zpush/backend_combined.php @@ -0,0 +1,49 @@ + array( + 'i' => array( + 'name' => 'BackendIMAP', + ), + 'c' => array( + 'name' => 'BackendCalDAV', + ), + 'd' => array( + 'name' => 'BackendCardDAV', + ), + ), + 'delimiter' => '/', + 'folderbackend' => array( + SYNC_FOLDER_TYPE_INBOX => 'i', + SYNC_FOLDER_TYPE_DRAFTS => 'i', + SYNC_FOLDER_TYPE_WASTEBASKET => 'i', + SYNC_FOLDER_TYPE_SENTMAIL => 'i', + SYNC_FOLDER_TYPE_OUTBOX => 'i', + SYNC_FOLDER_TYPE_TASK => 'c', + SYNC_FOLDER_TYPE_APPOINTMENT => 'c', + SYNC_FOLDER_TYPE_CONTACT => 'd', + SYNC_FOLDER_TYPE_NOTE => 'c', + SYNC_FOLDER_TYPE_JOURNAL => 'c', + SYNC_FOLDER_TYPE_OTHER => 'i', + SYNC_FOLDER_TYPE_USER_MAIL => 'i', + SYNC_FOLDER_TYPE_USER_APPOINTMENT => 'c', + SYNC_FOLDER_TYPE_USER_CONTACT => 'd', + SYNC_FOLDER_TYPE_USER_TASK => 'c', + SYNC_FOLDER_TYPE_USER_JOURNAL => 'c', + SYNC_FOLDER_TYPE_USER_NOTE => 'c', + SYNC_FOLDER_TYPE_UNKNOWN => 'i', + ), + 'rootcreatefolderbackend' => 'i', + ); + } +} + +?> diff --git a/conf/zpush/backend_imap.php b/conf/zpush/backend_imap.php new file mode 100644 index 0000000..4559409 --- /dev/null +++ b/conf/zpush/backend_imap.php @@ -0,0 +1,42 @@ + true))); +define('IMAP_FROM_SQL_QUERY', "select first_name, last_name, mail_address from users where mail_address = '#username@#domain'"); +define('IMAP_FROM_SQL_FIELDS', serialize(array('first_name', 'last_name', 'mail_address'))); +define('IMAP_FROM_SQL_FROM', '#first_name #last_name <#mail_address>'); +define('IMAP_FROM_LDAP_SERVER', ''); +define('IMAP_FROM_LDAP_SERVER_PORT', '389'); +define('IMAP_FROM_LDAP_USER', 'cn=zpush,ou=servers,dc=zpush,dc=org'); +define('IMAP_FROM_LDAP_PASSWORD', 'password'); +define('IMAP_FROM_LDAP_BASE', 'dc=zpush,dc=org'); +define('IMAP_FROM_LDAP_QUERY', '(mail=#username@#domain)'); +define('IMAP_FROM_LDAP_FIELDS', serialize(array('givenname', 'sn', 'mail'))); +define('IMAP_FROM_LDAP_FROM', '#givenname #sn <#mail>'); + + +// copy outgoing mail to this folder. If not set z-push will try the default folders +define('IMAP_SENTFOLDER', ''); +define('IMAP_INLINE_FORWARD', true); +define('IMAP_EXCLUDED_FOLDERS', ''); +define('IMAP_SMTP_METHOD', 'sendmail'); + +global $imap_smtp_params; +$imap_smtp_params = array('host' => 'ssl://localhost', 'port' => 587, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password'); + +define('MAIL_MIMEPART_CRLF', "\r\n"); + +?> diff --git a/management/web_update.py b/management/web_update.py index 0704c26..3ef2856 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -43,9 +43,10 @@ def do_web_update(env): nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read() # Add configuration for each web domain. - template = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read() + template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read() + template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read() for domain in get_web_domains(env): - nginx_conf += make_domain_config(domain, template, env) + nginx_conf += make_domain_config(domain, template1, template2, env) # Did the file change? If not, don't bother writing & restarting nginx. nginx_conf_fn = "/etc/nginx/conf.d/local.conf" @@ -63,7 +64,7 @@ def do_web_update(env): return "web updated\n" -def make_domain_config(domain, template, env): +def make_domain_config(domain, template, template_for_primaryhost, env): # How will we configure this domain. # Where will its root directory be for static files? @@ -77,25 +78,30 @@ def make_domain_config(domain, template, env): # available. Make a self-signed one now if one doesn't exist. ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, env) + # Put pieces together. + nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template) + nginx_conf = nginx_conf_parts[0] + "\n" + if domain == env['PRIMARY_HOSTNAME']: + nginx_conf += template_for_primaryhost + "\n" + # Replace substitution strings in the template & return. - nginx_conf = template + nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) # Add in any user customizations. - nginx_conf_parts = re.split("(# ADDITIONAL DIRECTIVES HERE\n)", nginx_conf) nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] if "proxy" in yaml: - nginx_conf_parts[1] += "\tlocation / {\n\t\tproxy_pass %s;\n\t}\n" % yaml["proxy"] + nginx_conf += "\tlocation / {\n\t\tproxy_pass %s;\n\t}\n" % yaml["proxy"] - # Put it all together. - nginx_conf = "".join(nginx_conf_parts) + # Ending. + nginx_conf += nginx_conf_parts[1] return nginx_conf diff --git a/setup/owncloud.sh b/setup/owncloud.sh new file mode 100755 index 0000000..7b63034 --- /dev/null +++ b/setup/owncloud.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# Owncloud +########################## + +source setup/functions.sh # load our functions +source /etc/mailinabox.conf # load global vars + +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 \ + php5 php5-dev php5-gd php5-fpm memcached php5-memcache unzip + +apt-get purge -qq -y owncloud* + +# Install ownCloud from source if it is not already present +# TODO: Check version? +if [ ! -d /usr/local/lib/owncloud ]; then + echo installing ownCloud... + rm -f /tmp/owncloud.zip + wget -qO /tmp/owncloud.zip https://download.owncloud.org/community/owncloud-7.0.1.zip + unzip -q /tmp/owncloud.zip -d /usr/local/lib + rm -f /tmp/owncloud.zip +fi + +# Setup ownCloud if the ownCloud 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 + # Create a configuration file. + TIMEZONE=`cat /etc/timezone` + instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1) + cat - > /usr/local/lib/owncloud/config/config.php < '$STORAGE_ROOT/owncloud', + + 'instanceid' => '$instanceid', + + 'trusted_domains' => + array ( + 0 => '$PRIMARY_HOSTNAME', + ), + 'forcessl' => true, # if unset/false, ownCloud sends a HSTS=0 header, which conflicts with nginx config + + 'overwritewebroot' => '/cloud', + 'user_backends' => array( + array( + 'class'=>'OC_User_IMAP', + 'arguments'=>array('{localhost:993/imap/ssl/novalidate-cert}') + ) + ), + "memcached_servers" => array ( + array('localhost', 11211), + ), + 'mail_smtpmode' => 'sendmail', + 'mail_smtpsecure' => '', + 'mail_smtpauthtype' => 'LOGIN', + 'mail_smtpauth' => false, + 'mail_smtphost' => '', + 'mail_smtpport' => '', + 'mail_smtpname' => '', + 'mail_smtppassword' => '', + 'mail_from_address' => 'owncloud', + 'mail_domain' => '$PRIMARY_HOSTNAME', + 'logtimezone' => '$TIMEZONE', +); +?> +EOF + + # Create an auto-configuration file to fill in database settings + # when the install script is run. Make an administrator account + # here or else the install can't finish. + adminpassword=$(dd if=/dev/random bs=40 count=1 2>/dev/null | sha1sum | fold -w 30 | head -n 1) + cat - > /usr/local/lib/owncloud/config/autoconfig.php < '$STORAGE_ROOT/owncloud', + 'dbtype' => 'sqlite3', + + # create an administrator account with a random password so that + # the user does not have to enter anything on first load of ownCloud + 'adminlogin' => 'root', + 'adminpass' => '$adminpassword', +); +?> +EOF + + # Create user data directory and set permissions + mkdir -p $STORAGE_ROOT/owncloud + chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud + + # Execute ownCloud's setup step, which creates the ownCloud sqlite database. + # It also wipes it if it exists. And it deletes the autoconfig.php file. + (cd /usr/local/lib/owncloud; sudo -u www-data php /usr/local/lib/owncloud/index.php;) +fi + +# Enable/disable apps. Note that this must be done after the ownCloud setup. +# The firstrunwizard gave Josh all sorts of problems, so disabling that. +# user_external is what allows ownCloud to use IMAP for login. +hide_output php /usr/local/lib/owncloud/console.php app:disable firstrunwizard +hide_output php /usr/local/lib/owncloud/console.php app:enable user_external + +# Set PHP FPM values to support large file uploads +# (semicolon is the comment character in this file, hashes produce deprecation warnings) +tools/editconf.py /etc/php5/fpm/php.ini -c ';' \ + upload_max_filesize=16G \ + post_max_size=16G \ + output_buffering=16384 \ + memory_limit=512M \ + max_execution_time=600 \ + short_open_tag=On + +# Set up a cron job for owncloud. +cat > /etc/cron.hourly/mailinabox-owncloud << EOF; +#!/bin/bash +# Mail-in-a-Box +sudo -u www-data php -f /usr/local/lib/owncloud/cron.php +EOF +chmod +x /etc/cron.hourly/mailinabox-owncloud + +## Ensure all system admins are ownCloud admins. +## Actually we don't do this. There's nothing much of interest that the user could +## change from the ownCloud admin, and there's a lot they could mess up. +#for user in $(tools/mail.py user admins); do +# sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "INSERT OR IGNORE INTO oc_group_user VALUES ('admin', '$user')" +#done + +# Finished. +php5enmod imap +restart_service php5-fpm diff --git a/setup/start.sh b/setup/start.sh index 0b06c9e..469f616 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -274,6 +274,7 @@ EOF . setup/spamassassin.sh . setup/web.sh . setup/webmail.sh +. setup/owncloud.sh . setup/zpush.sh . setup/management.sh diff --git a/setup/zpush.sh b/setup/zpush.sh index a65d71b..7496229 100755 --- a/setup/zpush.sh +++ b/setup/zpush.sh @@ -14,30 +14,51 @@ source /etc/mailinabox.conf # load global vars # Prereqs. apt_install \ - php-soap php5-imap + php-soap php5-imap libawl-php php5-xsl php5enmod imap # Copy Z-Push into place. - -if [ ! -d /usr/local/lib/z-push ]; then - ZPUSH=z-push-2.1.3-1892 - wget -qO /tmp/zpush.tgz http://download.z-push.org/final/2.1/$ZPUSH.tar.gz - tar -C /tmp -zxf /tmp/zpush.tgz - mv /tmp/$ZPUSH /usr/local/lib/z-push +needs_update=0 +if [ ! -f /usr/local/lib/z-push/version ]; then + needs_update=1 +elif [[ `curl -s https://api.github.com/repos/fmbiete/Z-Push-contrib/git/refs/heads/master` != `cat /usr/local/lib/z-push/version` ]]; then + # checks if the version + needs_update=1 +fi +if [ $needs_update == 1 ]; then + rm -rf /usr/local/lib/z-push + rm -f /tmp/zpush.zip + echo installing z-push \(fmbiete fork\)... + wget -qO /tmp/zpush.zip https://github.com/fmbiete/Z-Push-contrib/archive/master.zip + unzip -q /tmp/zpush.zip -d /usr/local/lib/ + mv /usr/local/lib/Z-Push-contrib-master /usr/local/lib/z-push + rm -f /usr/sbin/z-push-{admin,top} ln -s /usr/local/lib/z-push/z-push-admin.php /usr/sbin/z-push-admin ln -s /usr/local/lib/z-push/z-push-top.php /usr/sbin/z-push-top - rm /tmp/zpush.tgz; + rm /tmp/zpush.zip; + curl -s https://api.github.com/repos/fmbiete/Z-Push-contrib/git/refs/heads/master > /usr/local/lib/z-push/version fi -# Configure. Tell is to connect to email via IMAP using SSL. Since we connect on -# localhost, the certificate won't match (it may be self-signed and invalid anyway) -# so don't check the cert. -sed -i "s/define('BACKEND_PROVIDER', .*/define('BACKEND_PROVIDER', 'BackendIMAP');/" /usr/local/lib/z-push/config.php -#sed -i "s/define('IMAP_SERVER', .*/define('IMAP_SERVER', '$PRIMARY_HOSTNAME');/" /usr/local/lib/z-push/backend/imap/config.php -sed -i "s/define('IMAP_PORT', .*/define('IMAP_PORT', 993);/" /usr/local/lib/z-push/backend/imap/config.php -sed -i "s/define('IMAP_OPTIONS', .*/define('IMAP_OPTIONS', '\/ssl\/norsh\/novalidate-cert');/" /usr/local/lib/z-push/backend/imap/config.php +# Configure default config. +sed -i "s/define('TIMEZONE', .*/define('TIMEZONE', 'Etc\/UTC');/" /usr/local/lib/z-push/config.php +sed -i "s/define('BACKEND_PROVIDER', .*/define('BACKEND_PROVIDER', 'BackendCombined');/" /usr/local/lib/z-push/config.php +# Configure BACKEND +rm -f /usr/local/lib/z-push/backend/combined/config.php +cp conf/zpush/backend_combined.php /usr/local/lib/z-push/backend/combined/config.php + +# Configure IMAP +rm -f /usr/local/lib/z-push/backend/imap/config.php +cp conf/zpush/backend_imap.php /usr/local/lib/z-push/backend/imap/config.php + +# Configure CardDav +rm -f /usr/local/lib/z-push/backend/carddav/config.php +cp conf/zpush/backend_carddav.php /usr/local/lib/z-push/backend/carddav/config.php + +# Configure CalDav +rm -f /usr/local/lib/z-push/backend/caldav/config.php +cp conf/zpush/backend_caldav.php /usr/local/lib/z-push/backend/caldav/config.php # Some directories it will use. diff --git a/tools/editconf.py b/tools/editconf.py index e6e7c68..7bc3d19 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -33,6 +33,7 @@ settings = sys.argv[2:] delimiter = "=" delimiter_re = r"\s*=\s*" +comment_char = "#" folded_lines = False testing = False while settings[0][0] == "-" and settings[0] != "--": @@ -42,7 +43,11 @@ while settings[0][0] == "-" and settings[0] != "--": delimiter = " " delimiter_re = r"\s+" elif opt == "-w": + # Line folding is possible in this file. folded_lines = True + elif opt == "-c": + # Specifies a different comment character. + comment_char = settings.pop(0) elif opt == "-t": testing = True else: @@ -60,7 +65,7 @@ while len(input_lines) > 0: # If this configuration file uses folded lines, append any folded lines # into our input buffer. - if folded_lines and line[0] not in ("#", " ", ""): + if folded_lines and line[0] not in (comment_char, " ", ""): while len(input_lines) > 0 and input_lines[0][0] in " \t": line += input_lines.pop(0) @@ -68,7 +73,11 @@ while len(input_lines) > 0: for i in range(len(settings)): # Check that this line contain this setting from the command-line arguments. name, val = settings[i].split("=", 1) - m = re.match("(\s*)(#\s*)?" + re.escape(name) + delimiter_re + "(.*?)\s*$", line, re.S) + m = re.match( + "(\s*)" + + "(" + re.escape(comment_char) + "\s*)?" + + re.escape(name) + delimiter_re + "(.*?)\s*$", + line, re.S) if not m: continue indent, is_comment, existing_val = m.groups() @@ -83,7 +92,7 @@ while len(input_lines) > 0: # comment-out the existing line (also comment any folded lines) if is_comment is None: - buf += "#" + line.rstrip().replace("\n", "\n#") + "\n" + buf += comment_char + line.rstrip().replace("\n", "\n" + comment_char) + "\n" else: # the line is already commented, pass it through buf += line diff --git a/tools/mail.py b/tools/mail.py index ab54b75..ce2d3e4 100755 --- a/tools/mail.py +++ b/tools/mail.py @@ -51,6 +51,7 @@ if len(sys.argv) < 2: print(" tools/mail.py user remove user@domain.com") print(" tools/mail.py user make-admin user@domain.com") print(" tools/mail.py user remove-admin user@domain.com") + print(" tools/mail.py user admins (lists admins)") print(" tools/mail.py alias (lists aliases)") print(" tools/mail.py alias add incoming.name@domain.com sent.to@other.domain.com") print(" tools/mail.py alias remove incoming.name@domain.com") @@ -92,6 +93,13 @@ elif sys.argv[1] == "user" and sys.argv[2] in ("make-admin", "remove-admin") and action = "remove" print(mgmt("/mail/users/privileges/" + action, { "email": sys.argv[3], "privilege": "admin" })) +elif sys.argv[1] == "user" and sys.argv[2] == "admins": + # Dump a list of admin users. + users = mgmt("/mail/users?format=json", is_json=True) + for user in users: + if "admin" in user['privileges']: + print(user['email']) + elif sys.argv[1] == "alias" and len(sys.argv) == 2: print(mgmt("/mail/aliases"))