mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-12 17:07:23 +01:00
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94b7c80792 | ||
|
|
ae8cd4efdf | ||
|
|
6d259a6e12 | ||
|
|
e7fffc66c7 | ||
|
|
8548ede638 | ||
|
|
6eeb107ee3 | ||
|
|
31eefa18da | ||
|
|
20adbb51cb | ||
|
|
79a39d86f9 | ||
|
|
0ebf33e9df | ||
|
|
d3818d1db6 | ||
|
|
f65d9d3196 | ||
|
|
74fea6b93e | ||
|
|
7a935d8385 | ||
|
|
7e0f534aea | ||
|
|
5628f8eecb | ||
|
|
9cc5160c38 | ||
|
|
bc40134b7b | ||
|
|
3649ba1ce9 | ||
|
|
22395bdb8b | ||
|
|
30c89be982 | ||
|
|
853b641d1b | ||
|
|
703a963ae5 | ||
|
|
1a1d125b31 | ||
|
|
86881c0107 | ||
|
|
e65c77588e | ||
|
|
3843f63416 | ||
|
|
703e6795e8 | ||
|
|
b3223136f4 | ||
|
|
aa1fdaddaf | ||
|
|
7fa9baf308 | ||
|
|
36d51bbde0 | ||
|
|
eb8cfaab75 | ||
|
|
c5e8a975cd | ||
|
|
3210ccdcac | ||
|
|
252c35c66e | ||
|
|
c910a58f07 | ||
|
|
f292e8fc5b | ||
|
|
4d7229ccb0 | ||
|
|
1e1c3cbd00 | ||
|
|
611e9cc84d | ||
|
|
454a2b167b | ||
|
|
f6e0af124f | ||
|
|
d7d8bda0a4 | ||
|
|
df92a10eba | ||
|
|
74a0359cec | ||
|
|
336b95b3d5 | ||
|
|
56591abbc2 | ||
|
|
313a86d0fa | ||
|
|
083e3cf755 | ||
|
|
696bbe4e82 | ||
|
|
3d4cabbcd5 | ||
|
|
cdedaed3b0 | ||
|
|
c01f903413 | ||
|
|
5edefbec27 | ||
|
|
67555679bd | ||
|
|
546d6f0026 | ||
|
|
bd86d44c8b | ||
|
|
72fcb005b2 | ||
|
|
84638ab11e | ||
|
|
84f4509b48 | ||
|
|
35a593af13 | ||
|
|
f69d6e9015 | ||
|
|
44705a32b7 | ||
|
|
e343061cf4 | ||
|
|
65add24e2a | ||
|
|
33a9fb6aa2 | ||
|
|
0bc5d20e8f | ||
|
|
49ea9cddd1 | ||
|
|
6a48cdcdf3 | ||
|
|
f78f039776 | ||
|
|
d881487d68 | ||
|
|
33d07b2b54 | ||
|
|
3bbec18ac6 | ||
|
|
fc5c198646 | ||
|
|
2be373fd06 | ||
|
|
b71ad85e9f | ||
|
|
86d3e9da86 | ||
|
|
f53d3bc390 | ||
|
|
f9ca440ce8 | ||
|
|
8ea2f5a766 | ||
|
|
6c1357e16c | ||
|
|
d880f088be | ||
|
|
5cabfd591b | ||
|
|
721730f0e8 | ||
|
|
af80849857 | ||
|
|
7a191e67b8 | ||
|
|
4b2e48f2c0 | ||
|
|
eb545d7941 | ||
|
|
a2e6e81697 | ||
|
|
1b24e2cbaf | ||
|
|
0843159fb4 | ||
|
|
a7e60af93f | ||
|
|
42f879687f | ||
|
|
057903a303 | ||
|
|
b8e99c30a2 | ||
|
|
3d933c16d0 | ||
|
|
e785886447 | ||
|
|
23ecff04b8 | ||
|
|
a0bae5db5c | ||
|
|
86368ed165 | ||
|
|
5e4c0ed825 | ||
|
|
ffa9dc5d67 | ||
|
|
43cb6c4995 | ||
|
|
36cb2ef41d | ||
|
|
098e250cc4 | ||
|
|
3d5a35b184 | ||
|
|
87d3f2641d | ||
|
|
c6c75c5a17 | ||
|
|
1ba44b02d4 | ||
|
|
6fd4cd85ca | ||
|
|
6182347641 | ||
|
|
401b0526a3 | ||
|
|
2f24328608 | ||
|
|
8ea42847da | ||
|
|
4ed23f44e6 | ||
|
|
178527dab1 | ||
|
|
f5c376dca8 | ||
|
|
239eac662c | ||
|
|
4e18f66db6 | ||
|
|
77937df955 | ||
|
|
4db8efa0df | ||
|
|
66c80bd16a | ||
|
|
5895aeecd7 | ||
|
|
83ffc99b9c | ||
|
|
85a9a1608c | ||
|
|
2e693f7011 | ||
|
|
6f0220da4b | ||
|
|
09a45b4397 | ||
|
|
6b408ef824 | ||
|
|
8932aaf4ef | ||
|
|
6d6f3ea391 |
90
CHANGELOG.md
90
CHANGELOG.md
@@ -1,10 +1,98 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
v0.18 (May 15, 2016)
|
||||
--------------------
|
||||
|
||||
ownCloud:
|
||||
|
||||
* Updated to ownCloud to 8.2.3
|
||||
|
||||
Mail:
|
||||
|
||||
* Roundcube is updated to version 1.1.5 and the Roundcube login screen now says "[hostname] Webmail" instead of "Mail-in-a-Box/Roundcube webmail".
|
||||
* Fixed a long-standing issue with training the spam filter not working (because of a file permissions issue).
|
||||
|
||||
Control panel:
|
||||
|
||||
* Munin system monitoring graphs are now zoomable.
|
||||
* When a reboot is required (due to Ubuntu security updates automatically installed), a Reboot Box button now appears on the System Status Checks page of the control panel.
|
||||
* It is now possible to add SRV and secondary MX records in the Custom DNS page.
|
||||
* Other minor fixes.
|
||||
|
||||
System:
|
||||
|
||||
* The fail2ban recidive jail, which blocks long-duration brute force attacks, now no longer sends the administrator emails (which were not helpful).
|
||||
|
||||
Setup:
|
||||
|
||||
* The system hostname is now set during setup.
|
||||
* A swap file is now created if system memory is less than 2GB, 5GB of free disk space is available, and if no swap file yet exists.
|
||||
* We now install Roundcube from the official GitHub repository instead of our own mirror, which we had previously created to solve problems with SourceForge.
|
||||
* DKIM was incorrectly set up on machines where "localhost" was defined as something other than "127.0.0.1".
|
||||
|
||||
v0.17c (April 1, 2016)
|
||||
----------------------
|
||||
|
||||
This update addresses some minor security concerns and some installation issues.
|
||||
|
||||
ownCoud:
|
||||
|
||||
* Block web access to the configuration parameters (config.php). There is no immediate impact (see [#776](https://github.com/mail-in-a-box/mailinabox/pull/776)), although advanced users may want to take note.
|
||||
|
||||
Mail:
|
||||
|
||||
* Roundcube html5_notifier plugin updated from version 0.6 to 0.6.2 to fix Roundcube getting stuck for some people.
|
||||
|
||||
Control panel:
|
||||
|
||||
* Prevent click-jacking of the management interface by adding HTTP headers.
|
||||
* Failed login no longer reveals whether an account exists on the system.
|
||||
|
||||
Setup:
|
||||
|
||||
* Setup dialogs did not appear correctly when connecting to SSH using Putty on Windows.
|
||||
* We now install Roundcube from our own mirror because Sourceforge's downloads experience frequent intermittant unavailability.
|
||||
|
||||
v0.17b (March 1, 2016)
|
||||
----------------------
|
||||
|
||||
ownCloud moved their source code to a new location, breaking our installation script.
|
||||
|
||||
v0.17 (February 25, 2016)
|
||||
-------------------------
|
||||
|
||||
Mail:
|
||||
|
||||
* Roundcube updated to version 1.1.4.
|
||||
* When there's a problem delivering an outgoing message, a new 'warning' bounce will come after 3 hours and the box will stop trying after 2 days (instead of 5).
|
||||
* On multi-homed machines, Postfix now binds to the right network interface when sending outbound mail so that SPF checks on the receiving end will pass.
|
||||
* Mail sent from addresses on subdomains of other domains hosted by this box would not be DKIM-signed and so would fail DMARC checks by recipients, since version v0.15.
|
||||
|
||||
Control panel:
|
||||
|
||||
* TLS certificate provisioning would crash if DNS propagation was in progress and a challenge failed; might have shown the wrong error when provisioning fails.
|
||||
* Backup times were displayed with the wrong time zone.
|
||||
* Thresholds for displaying messages when the system is running low on memory have been reduced from 30% to 20% for a warning and from 15% to 10% for an error.
|
||||
* Other minor fixes.
|
||||
|
||||
System:
|
||||
|
||||
* Backups to some AWS S3 regions broke in version 0.15 because we reverted the version of boto. That's now fixed.
|
||||
* On low-usage systems, don't hold backups for quite so long by taking a full backup more often.
|
||||
* Nightly status checks might fail on systems not configured with a default Unicode locale.
|
||||
* If domains need a TLS certificate and the user hasn't installed one yet using Let's Encrypt, the administrator would get a nightly email with weird interactive text asking them to agree to Let's Encrypt's ToS. Now just say that the provisioning can't be done automatically.
|
||||
* Reduce the number of background processes used by the management daemon to lower memory consumption.
|
||||
|
||||
Setup:
|
||||
|
||||
* The first screen now warns users not to install on a machine used for other things.
|
||||
|
||||
v0.16 (January 30, 2016)
|
||||
------------------------
|
||||
|
||||
This update primarily adds automatica SSL (now "TLS") certificate provisioning from Let's Encrypt (https://letsencrypt.org/).
|
||||
This update primarily adds automatic SSL (now "TLS") certificate provisioning from Let's Encrypt (https://letsencrypt.org/).
|
||||
* The Sieve port is now open so tools like the Thunderbird Sieve program can be used to edit mail filters.
|
||||
|
||||
Control Panel:
|
||||
|
||||
|
||||
@@ -59,20 +59,20 @@ by me:
|
||||
$ curl -s https://keybase.io/joshdata/key.asc | gpg --import
|
||||
gpg: key C10BDD81: public key "Joshua Tauberer <jt@occams.info>" imported
|
||||
|
||||
$ git verify-tag v0.16
|
||||
$ git verify-tag v0.18
|
||||
gpg: Signature made ..... using RSA key ID C10BDD81
|
||||
gpg: Good signature from "Joshua Tauberer <jt@occams.info>"
|
||||
gpg: WARNING: This key is not certified with a trusted signature!
|
||||
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 matchs the
|
||||
You'll get a lot of warnings, but that's 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.)
|
||||
|
||||
Checkout the tag corresponding to the most recent release:
|
||||
|
||||
$ git checkout v0.16
|
||||
$ git checkout v0.17c
|
||||
|
||||
Begin the installation.
|
||||
|
||||
|
||||
@@ -27,3 +27,14 @@ maxretry = 20
|
||||
[recidive]
|
||||
enabled = true
|
||||
maxretry = 10
|
||||
action = iptables-allports[name=recidive]
|
||||
# In the recidive section of jail.conf the action contains:
|
||||
#
|
||||
# action = iptables-allports[name=recidive]
|
||||
# sendmail-whois-lines[name=recidive, logpath=/var/log/fail2ban.log]
|
||||
#
|
||||
# The last line on the action will sent an email to the configured address. This mail will
|
||||
# notify the administrator that someone has been repeatedly triggering one of the other jails.
|
||||
# By default we don't configure this address and no action is required from the admin anyway.
|
||||
# So the notification is ommited. This will prevent message appearing in the mail.log that mail
|
||||
# can't be delivered to fail2ban@$HOSTNAME.
|
||||
|
||||
@@ -27,9 +27,9 @@ EXEC_AS_USER=root
|
||||
|
||||
# Ensure Python reads/writes files in UTF-8. If the machine
|
||||
# triggers some other locale in Python, like ASCII encoding,
|
||||
# Python may not be able to read/write files. Here and in
|
||||
# Python may not be able to read/write files. Set also
|
||||
# setup/start.sh (where the locale is also installed if not
|
||||
# already present).
|
||||
# already present) and management/daily_tasks.sh.
|
||||
export LANGUAGE=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
export LANG=en_US.UTF-8
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
location /admin/ {
|
||||
proxy_pass http://127.0.0.1:10222/;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
add_header X-Frame-Options "DENY";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Content-Security-Policy "frame-ancestors 'none';";
|
||||
}
|
||||
|
||||
# ownCloud configuration.
|
||||
@@ -15,8 +18,11 @@
|
||||
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/(build|tests|config|lib|3rdparty|templates|data|README)/ {
|
||||
deny all;
|
||||
}
|
||||
location ~ ^/cloud/(?:\.|autotest|occ|issue|indie|db_|console) {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
location ~ ^(/cloud)((?:/ocs)?/[^/]+\.php)(/.*)?$ {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
************************************************/
|
||||
|
||||
define('CALDAV_PROTOCOL', 'https');
|
||||
define('CALDAV_SERVER', 'localhost');
|
||||
define('CALDAV_SERVER', '127.0.0.1');
|
||||
define('CALDAV_PORT', '443');
|
||||
define('CALDAV_PATH', '/caldav/calendars/%u/');
|
||||
define('CALDAV_PERSONAL', 'PRINCIPAL');
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
|
||||
define('CARDDAV_PROTOCOL', 'https'); /* http or https */
|
||||
define('CARDDAV_SERVER', 'localhost');
|
||||
define('CARDDAV_SERVER', '127.0.0.1');
|
||||
define('CARDDAV_PORT', '443');
|
||||
define('CARDDAV_PATH', '/carddav/addressbooks/%u/');
|
||||
define('CARDDAV_DEFAULT_PATH', '/carddav/addressbooks/%u/contacts/'); /* subdirectory of the main path */
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Descr : IMAP backend configuration file
|
||||
************************************************/
|
||||
|
||||
define('IMAP_SERVER', 'localhost');
|
||||
define('IMAP_SERVER', '127.0.0.1');
|
||||
define('IMAP_PORT', 993);
|
||||
define('IMAP_OPTIONS', '/ssl/norsh/novalidate-cert');
|
||||
define('IMAP_DEFAULTFROM', '');
|
||||
@@ -44,7 +44,7 @@ define('IMAP_FROM_LDAP_FROM', '#givenname #sn <#mail>');
|
||||
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');
|
||||
$imap_smtp_params = array('host' => 'ssl://127.0.0.1', 'port' => 587, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password');
|
||||
|
||||
define('MAIL_MIMEPART_CRLF', "\r\n");
|
||||
|
||||
|
||||
@@ -42,10 +42,10 @@ def backup_status(env):
|
||||
# Get duplicity collection status and parse for a list of backups.
|
||||
def parse_line(line):
|
||||
keys = line.strip().split()
|
||||
date = dateutil.parser.parse(keys[1])
|
||||
date = dateutil.parser.parse(keys[1]).astimezone(dateutil.tz.tzlocal())
|
||||
return {
|
||||
"date": keys[1],
|
||||
"date_str": date.strftime("%x %X"),
|
||||
"date_str": date.strftime("%x %X") + " " + now.tzname(),
|
||||
"date_delta": reldate(date, now, "the future?"),
|
||||
"full": keys[0] == "full",
|
||||
"size": 0, # collection-status doesn't give us the size
|
||||
@@ -81,50 +81,66 @@ def backup_status(env):
|
||||
# This is relied on by should_force_full() and the next step.
|
||||
backups = sorted(backups.values(), key = lambda b : b["date"], reverse=True)
|
||||
|
||||
# Get the average size of incremental backups and the size of the
|
||||
# most recent full backup.
|
||||
# Get the average size of incremental backups, the size of the
|
||||
# most recent full backup, and the date of the most recent
|
||||
# backup and the most recent full backup.
|
||||
incremental_count = 0
|
||||
incremental_size = 0
|
||||
first_date = None
|
||||
first_full_size = None
|
||||
first_full_date = None
|
||||
for bak in backups:
|
||||
if first_date is None:
|
||||
first_date = dateutil.parser.parse(bak["date"])
|
||||
if bak["full"]:
|
||||
first_full_size = bak["size"]
|
||||
first_full_date = dateutil.parser.parse(bak["date"])
|
||||
break
|
||||
incremental_count += 1
|
||||
incremental_size += bak["size"]
|
||||
|
||||
# Predict how many more increments until the next full backup,
|
||||
# and add to that the time we hold onto backups, to predict
|
||||
# how long the most recent full backup+increments will be held
|
||||
# onto. Round up since the backup occurs on the night following
|
||||
# when the threshold is met.
|
||||
# When will the most recent backup be deleted? It won't be deleted if the next
|
||||
# backup is incremental, because the increments rely on all past increments.
|
||||
# So first guess how many more incremental backups will occur until the next
|
||||
# full backup. That full backup frees up this one to be deleted. But, the backup
|
||||
# must also be at least min_age_in_days old too.
|
||||
deleted_in = None
|
||||
if incremental_count > 0 and first_full_size is not None:
|
||||
deleted_in = "approx. %d days" % round(config["min_age_in_days"] + (.5 * first_full_size - incremental_size) / (incremental_size/incremental_count) + .5)
|
||||
# How many days until the next incremental backup? First, the part of
|
||||
# the algorithm based on increment sizes:
|
||||
est_days_to_next_full = (.5 * first_full_size - incremental_size) / (incremental_size/incremental_count)
|
||||
est_time_of_next_full = first_date + datetime.timedelta(days=est_days_to_next_full)
|
||||
|
||||
# When will a backup be deleted?
|
||||
# ...And then the part of the algorithm based on full backup age:
|
||||
est_time_of_next_full = min(est_time_of_next_full, first_full_date + datetime.timedelta(days=config["min_age_in_days"]*10+1))
|
||||
|
||||
# It still can't be deleted until it's old enough.
|
||||
est_deleted_on = max(est_time_of_next_full, first_date + datetime.timedelta(days=config["min_age_in_days"]))
|
||||
|
||||
deleted_in = "approx. %d days" % round((est_deleted_on-now).total_seconds()/60/60/24 + .5)
|
||||
|
||||
# When will a backup be deleted? Set the deleted_in field of each backup.
|
||||
saw_full = False
|
||||
days_ago = now - datetime.timedelta(days=config["min_age_in_days"])
|
||||
for bak in backups:
|
||||
if deleted_in:
|
||||
# Subsequent backups are deleted when the most recent increment
|
||||
# in the chain would be deleted.
|
||||
# The most recent increment in a chain and all of the previous backups
|
||||
# it relies on are deleted at the same time.
|
||||
bak["deleted_in"] = deleted_in
|
||||
if bak["full"]:
|
||||
# Reset when we get to a full backup. A new chain start next.
|
||||
# Reset when we get to a full backup. A new chain start *next*.
|
||||
saw_full = True
|
||||
deleted_in = None
|
||||
elif saw_full and not deleted_in:
|
||||
# Mark deleted_in only on the first increment after a full backup.
|
||||
deleted_in = reldate(days_ago, dateutil.parser.parse(bak["date"]), "on next daily backup")
|
||||
# We're now on backups prior to the most recent full backup. These are
|
||||
# free to be deleted as soon as they are min_age_in_days old.
|
||||
deleted_in = reldate(now, dateutil.parser.parse(bak["date"]) + datetime.timedelta(days=config["min_age_in_days"]), "on next daily backup")
|
||||
bak["deleted_in"] = deleted_in
|
||||
|
||||
return {
|
||||
"tz": now.tzname(),
|
||||
"backups": backups,
|
||||
}
|
||||
|
||||
def should_force_full(env):
|
||||
def should_force_full(config, env):
|
||||
# Force a full backup when the total size of the increments
|
||||
# since the last full backup is greater than half the size
|
||||
# of that full backup.
|
||||
@@ -136,8 +152,14 @@ def should_force_full(env):
|
||||
inc_size += bak["size"]
|
||||
else:
|
||||
# ...until we reach the most recent full backup.
|
||||
# Return if we should to a full backup.
|
||||
return inc_size > .5*bak["size"]
|
||||
# Return if we should to a full backup, which is based
|
||||
# on the size of the increments relative to the full
|
||||
# backup, as well as the age of the full backup.
|
||||
if inc_size > .5*bak["size"]:
|
||||
return True
|
||||
if dateutil.parser.parse(bak["date"]) + datetime.timedelta(days=config["min_age_in_days"]*10+1) < datetime.datetime.now(dateutil.tz.tzlocal()):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
# If we got here there are no (full) backups, so make one.
|
||||
# (I love for/else blocks. Here it's just to show off.)
|
||||
@@ -216,7 +238,7 @@ def perform_backup(full_backup):
|
||||
# the increments since the most recent full backup are
|
||||
# large.
|
||||
try:
|
||||
full_backup = full_backup or should_force_full(env)
|
||||
full_backup = full_backup or should_force_full(config, env)
|
||||
except Exception as e:
|
||||
# This was the first call to duplicity, and there might
|
||||
# be an error already.
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os, os.path, re, json
|
||||
|
||||
import subprocess
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask, request, render_template, abort, Response, send_from_directory
|
||||
from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response
|
||||
|
||||
import auth, utils
|
||||
import auth, utils, multiprocessing.pool
|
||||
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
||||
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
|
||||
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
|
||||
|
||||
# Create a worker pool for the status checks. The pool should
|
||||
# live across http requests so we don't baloon the system with
|
||||
# processes.
|
||||
import multiprocessing.pool
|
||||
pool = multiprocessing.pool.Pool(processes=10)
|
||||
|
||||
env = utils.load_environment()
|
||||
|
||||
auth_service = auth.KeyAuthService()
|
||||
@@ -49,7 +43,7 @@ def authorized_personnel_only(viewfunc):
|
||||
except ValueError as e:
|
||||
# Authentication failed.
|
||||
privs = []
|
||||
error = str(e)
|
||||
error = "Incorrect username or password"
|
||||
|
||||
# Authorized to access an API view?
|
||||
if "admin" in privs:
|
||||
@@ -125,7 +119,7 @@ def me():
|
||||
except ValueError as e:
|
||||
return json_response({
|
||||
"status": "invalid",
|
||||
"reason": str(e),
|
||||
"reason": "Incorrect username or password",
|
||||
})
|
||||
|
||||
resp = {
|
||||
@@ -436,7 +430,10 @@ def system_status():
|
||||
def print_line(self, message, monospace=False):
|
||||
self.items[-1]["extra"].append({ "text": message, "monospace": monospace })
|
||||
output = WebOutput()
|
||||
# Create a temporary pool of processes for the status checks
|
||||
pool = multiprocessing.pool.Pool(processes=5)
|
||||
run_checks(False, env, output, pool)
|
||||
pool.terminate()
|
||||
return json_response(output.items)
|
||||
|
||||
@app.route('/system/updates')
|
||||
@@ -456,6 +453,27 @@ def do_updates():
|
||||
"DEBIAN_FRONTEND": "noninteractive"
|
||||
})
|
||||
|
||||
|
||||
@app.route('/system/reboot', methods=["GET"])
|
||||
@authorized_personnel_only
|
||||
def needs_reboot():
|
||||
from status_checks import is_reboot_needed_due_to_package_installation
|
||||
if is_reboot_needed_due_to_package_installation():
|
||||
return json_response(True)
|
||||
else:
|
||||
return json_response(False)
|
||||
|
||||
@app.route('/system/reboot', methods=["POST"])
|
||||
@authorized_personnel_only
|
||||
def do_reboot():
|
||||
# To keep the attack surface low, we don't allow a remote reboot if one isn't necessary.
|
||||
from status_checks import is_reboot_needed_due_to_package_installation
|
||||
if is_reboot_needed_due_to_package_installation():
|
||||
return utils.shell("check_output", ["/sbin/shutdown", "-r", "now"], capture_stderr=True)
|
||||
else:
|
||||
return "No reboot is required, so it is not allowed."
|
||||
|
||||
|
||||
@app.route('/system/backup/status')
|
||||
@authorized_personnel_only
|
||||
def backup_status():
|
||||
@@ -507,6 +525,64 @@ def munin(filename=""):
|
||||
if filename == "": filename = "index.html"
|
||||
return send_from_directory("/var/cache/munin/www", filename)
|
||||
|
||||
@app.route('/munin/cgi-graph/<path:filename>')
|
||||
@authorized_personnel_only
|
||||
def munin_cgi(filename):
|
||||
""" Relay munin cgi dynazoom requests
|
||||
/usr/lib/munin/cgi/munin-cgi-graph is a perl cgi script in the munin package
|
||||
that is responsible for generating binary png images _and_ associated HTTP
|
||||
headers based on parameters in the requesting URL. All output is written
|
||||
to stdout which munin_cgi splits into response headers and binary response
|
||||
data.
|
||||
munin-cgi-graph reads environment variables as well as passed input to determine
|
||||
what it should do. It expects a path to be in the env-var PATH_INFO, and a
|
||||
querystring to be in the env-var QUERY_STRING as well as passed as input to the
|
||||
command.
|
||||
munin-cgi-graph has several failure modes. Some write HTTP Status headers and
|
||||
others return nonzero exit codes.
|
||||
Situating munin_cgi between the user-agent and munin-cgi-graph enables keeping
|
||||
the cgi script behind mailinabox's auth mechanisms and avoids additional
|
||||
support infrastructure like spawn-fcgi.
|
||||
"""
|
||||
|
||||
COMMAND = 'su - munin --preserve-environment --shell=/bin/bash -c /usr/lib/munin/cgi/munin-cgi-graph "%s"'
|
||||
# su changes user, we use the munin user here
|
||||
# --preserve-environment retains the environment, which is where Popen's `env` data is
|
||||
# --shell=/bin/bash ensures the shell used is bash
|
||||
# -c "/usr/lib/munin/cgi/munin-cgi-graph" passes the command to run as munin
|
||||
# "%s" is a placeholder for where the request's querystring will be added
|
||||
|
||||
if filename == "":
|
||||
return ("a path must be specified", 404)
|
||||
|
||||
query_str = request.query_string.decode("utf-8", 'ignore')
|
||||
|
||||
env = {'PATH_INFO': '/%s/' % filename, 'QUERY_STRING': query_str}
|
||||
cmd = COMMAND % query_str
|
||||
code, binout = utils.shell('check_output',
|
||||
cmd.split(' ', 5),
|
||||
# Using a maxsplit of 5 keeps the last 2 arguments together
|
||||
input=query_str.encode('UTF-8'),
|
||||
env=env,
|
||||
return_bytes=True,
|
||||
trap=True)
|
||||
|
||||
if code != 0:
|
||||
# nonzero returncode indicates error
|
||||
app.logger.error("munin_cgi: munin-cgi-graph returned nonzero exit code, %s", process.returncode)
|
||||
return ("error processing graph image", 500)
|
||||
|
||||
# /usr/lib/munin/cgi/munin-cgi-graph returns both headers and binary png when successful.
|
||||
# A double-Windows-style-newline always indicates the end of HTTP headers.
|
||||
headers, image_bytes = binout.split(b'\r\n\r\n', 1)
|
||||
response = make_response(image_bytes)
|
||||
for line in headers.splitlines():
|
||||
name, value = line.decode("utf8").split(':', 1)
|
||||
response.headers[name] = value
|
||||
if 'Status' in response.headers and '404' in response.headers['Status']:
|
||||
app.logger.warning("munin_cgi: munin-cgi-graph returned 404 status code. PATH_INFO=%s", env['PATH_INFO'])
|
||||
return response
|
||||
|
||||
# APP
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
#!/bin/bash
|
||||
# This script is run daily (at 3am each night).
|
||||
|
||||
# Set character encoding flags to ensure that any non-ASCII
|
||||
# characters don't cause problems. See setup/start.sh and
|
||||
# the management daemon startup script.
|
||||
export LANGUAGE=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_TYPE=en_US.UTF-8
|
||||
|
||||
# Take a backup.
|
||||
management/backup.py | management/email_administrator.py "Backup Status"
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ def do_dns_update(env, force=False):
|
||||
shell('check_call', ["/usr/sbin/service", "nsd", "restart"])
|
||||
|
||||
# Write the OpenDKIM configuration tables for all of the domains.
|
||||
if write_opendkim_tables([domain for domain, zonefile in zonefiles], env):
|
||||
if write_opendkim_tables(get_mail_domains(env), env):
|
||||
# Settings changed. Kick opendkim.
|
||||
shell('check_call', ["/usr/sbin/service", "opendkim", "restart"])
|
||||
if len(updated_domains) == 0:
|
||||
@@ -175,9 +175,6 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
|
||||
for value in build_sshfp_records():
|
||||
records.append((None, "SSHFP", value, "Optional. Provides an out-of-band method for verifying an SSH key before connecting. Use 'VerifyHostKeyDNS yes' (or 'VerifyHostKeyDNS ask') when connecting with ssh."))
|
||||
|
||||
# The MX record says where email for the domain should be delivered: Here!
|
||||
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
|
||||
|
||||
# Add DNS records for any subdomains of this domain. We should not have a zone for
|
||||
# both a domain and one of its subdomains.
|
||||
subdomains = [d for d in all_domains if d.endswith("." + domain)]
|
||||
@@ -244,6 +241,10 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
|
||||
# Don't pin the list of records that has_rec checks against anymore.
|
||||
has_rec_base = records
|
||||
|
||||
# The MX record says where email for the domain should be delivered: Here!
|
||||
if not has_rec(None, "MX", prefix="10 "):
|
||||
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
|
||||
|
||||
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
|
||||
# the domain, and no one else.
|
||||
# Skip if the user has set a custom SPF record.
|
||||
|
||||
@@ -33,7 +33,7 @@ msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject)
|
||||
msg.set_payload(content, "UTF-8")
|
||||
|
||||
# send
|
||||
smtpclient = smtplib.SMTP('localhost', 25)
|
||||
smtpclient = smtplib.SMTP('127.0.0.1', 25)
|
||||
smtpclient.ehlo()
|
||||
smtpclient.sendmail(
|
||||
admin_addr, # MAIL FROM
|
||||
|
||||
@@ -137,19 +137,20 @@ def get_mail_users_ex(env, with_archived=False, with_slow_info=False):
|
||||
if with_archived:
|
||||
root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes')
|
||||
for domain in os.listdir(root):
|
||||
for user in os.listdir(os.path.join(root, domain)):
|
||||
email = user + "@" + domain
|
||||
mbox = os.path.join(root, domain, user)
|
||||
if email in active_accounts: continue
|
||||
user = {
|
||||
"email": email,
|
||||
"privileges": "",
|
||||
"status": "inactive",
|
||||
"mailbox": mbox,
|
||||
}
|
||||
users.append(user)
|
||||
if with_slow_info:
|
||||
user["mailbox_size"] = utils.du(mbox)
|
||||
if os.path.isdir(os.path.join(root, domain)):
|
||||
for user in os.listdir(os.path.join(root, domain)):
|
||||
email = user + "@" + domain
|
||||
mbox = os.path.join(root, domain, user)
|
||||
if email in active_accounts: continue
|
||||
user = {
|
||||
"email": email,
|
||||
"privileges": "",
|
||||
"status": "inactive",
|
||||
"mailbox": mbox,
|
||||
}
|
||||
users.append(user)
|
||||
if with_slow_info:
|
||||
user["mailbox_size"] = utils.du(mbox)
|
||||
|
||||
# Group by domain.
|
||||
domains = { }
|
||||
|
||||
@@ -204,7 +204,7 @@ def get_certificates_to_provision(env, show_extended_problems=True, force_domain
|
||||
domains_if_any.add(domain)
|
||||
|
||||
# It's valid. Should we report its validness?
|
||||
if show_extended_problems:
|
||||
elif show_extended_problems:
|
||||
problems[domain] = "The certificate is valid for at least another 30 days --- no need to replace."
|
||||
|
||||
# Warn the user about domains hosted elsewhere.
|
||||
@@ -365,7 +365,7 @@ def provision_certificates(env, agree_to_tos_url=None, logger=None, show_extende
|
||||
"message": "Something unexpected went wrong. It looks like your local Let's Encrypt account data is corrupted. There was a problem with the file " + e.account_file_path + ".",
|
||||
})
|
||||
|
||||
except (client.InvalidDomainName, client.NeedToTakeAction, acme.messages.Error, requests.exceptions.RequestException) as e:
|
||||
except (client.InvalidDomainName, client.NeedToTakeAction, client.ChallengeFailed, client.RateLimited, acme.messages.Error, requests.exceptions.RequestException) as e:
|
||||
ret_item.update({
|
||||
"result": "error",
|
||||
"message": "Something unexpected went wrong: " + str(e),
|
||||
@@ -458,9 +458,14 @@ def provision_certificates_cmdline():
|
||||
if agree_to_tos_url is not None:
|
||||
continue
|
||||
|
||||
# Can't ask the user a question in this mode.
|
||||
if headless in sys.argv:
|
||||
print("Can't issue TLS certficate until user has agreed to Let's Encrypt TOS.")
|
||||
# Can't ask the user a question in this mode. Warn the user that something
|
||||
# needs to be done.
|
||||
if headless:
|
||||
print(", ".join(request["domains"]) + " need a new or renewed TLS certificate.")
|
||||
print()
|
||||
print("This box can't do that automatically for you until you agree to Let's Encrypt's")
|
||||
print("Terms of Service agreement. Use the Mail-in-a-Box control panel to provision")
|
||||
print("certificates for these domains.")
|
||||
sys.exit(1)
|
||||
|
||||
print("""
|
||||
@@ -513,7 +518,7 @@ Do you agree to the agreement? Type Y or N and press <ENTER>: """
|
||||
print("A TLS certificate was requested for: " + ", ".join(wait_domains) + ".")
|
||||
first = True
|
||||
while wait_until > datetime.datetime.now():
|
||||
if "--headless" not in sys.argv or first:
|
||||
if not headless or first:
|
||||
print ("We have to wait", int(round((wait_until - datetime.datetime.now()).total_seconds())), "seconds for the certificate to be issued...")
|
||||
time.sleep(10)
|
||||
first = False
|
||||
|
||||
@@ -185,10 +185,13 @@ def check_ssh_password(env, output):
|
||||
else:
|
||||
output.print_ok("SSH disallows password-based login.")
|
||||
|
||||
def is_reboot_needed_due_to_package_installation():
|
||||
return os.path.exists("/var/run/reboot-required")
|
||||
|
||||
def check_software_updates(env, output):
|
||||
# Check for any software package updates.
|
||||
pkgs = list_apt_updates(apt_update=False)
|
||||
if os.path.exists("/var/run/reboot-required"):
|
||||
if is_reboot_needed_due_to_package_installation():
|
||||
output.print_error("System updates have been installed and a reboot of the machine is required.")
|
||||
elif len(pkgs) == 0:
|
||||
output.print_ok("System software is up to date.")
|
||||
@@ -222,14 +225,14 @@ def check_free_memory(rounded_values, env, output):
|
||||
# Check free memory.
|
||||
percent_free = 100 - psutil.virtual_memory().percent
|
||||
memory_msg = "System memory is %s%% free." % str(round(percent_free))
|
||||
if percent_free >= 30:
|
||||
if rounded_values: memory_msg = "System free memory is at least 30%."
|
||||
if percent_free >= 20:
|
||||
if rounded_values: memory_msg = "System free memory is at least 20%."
|
||||
output.print_ok(memory_msg)
|
||||
elif percent_free >= 15:
|
||||
if rounded_values: memory_msg = "System free memory is below 30%."
|
||||
elif percent_free >= 10:
|
||||
if rounded_values: memory_msg = "System free memory is below 20%."
|
||||
output.print_warning(memory_msg)
|
||||
else:
|
||||
if rounded_values: memory_msg = "System free memory is below 15%."
|
||||
if rounded_values: memory_msg = "System free memory is below 10%."
|
||||
output.print_error(memory_msg)
|
||||
|
||||
def run_network_checks(env, output):
|
||||
@@ -464,7 +467,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||
elif ip is None:
|
||||
output.print_error("Secondary nameserver %s is not configured to resolve this domain." % ns)
|
||||
else:
|
||||
output.print_error("Secondary nameserver %s is not configured correctly. (It resolved this domain as %s. It should be %s.)" % (ns, ip, env['PUBLIC_IP']))
|
||||
output.print_error("Secondary nameserver %s is not configured correctly. (It resolved this domain as %s. It should be %s.)" % (ns, ip, correct_ip))
|
||||
|
||||
def check_dns_zone_suggestions(domain, env, output, dns_zonefiles, domains_with_a_records):
|
||||
# Warn if a custom DNS record is preventing this or the automatic www redirect from
|
||||
@@ -740,10 +743,10 @@ def what_version_is_this(env):
|
||||
return tag
|
||||
|
||||
def get_latest_miab_version():
|
||||
# This pings https://mailinabox.email/bootstrap.sh and extracts the tag named in
|
||||
# This pings https://mailinabox.email/setup.sh and extracts the tag named in
|
||||
# the script to determine the current product version.
|
||||
import urllib.request
|
||||
return re.search(b'TAG=(.*)', urllib.request.urlopen("https://mailinabox.email/bootstrap.sh?ping=1").read()).group(1).decode("utf8")
|
||||
return re.search(b'TAG=(.*)', urllib.request.urlopen("https://mailinabox.email/setup.sh?ping=1").read()).group(1).decode("utf8")
|
||||
|
||||
def check_miab_version(env, output):
|
||||
config = load_settings(env)
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<option value="CNAME" data-hint="Enter another domain name followed by a period at the end (e.g. mypage.github.io.).">CNAME (DNS forwarding)</option>
|
||||
<option value="TXT" data-hint="Enter arbitrary text.">TXT (text record)</option>
|
||||
<option value="MX" data-hint="Enter record in the form of PRIORIY DOMAIN., including trailing period (e.g. 20 mx.example.com.).">MX (mail exchanger)</option>
|
||||
<option value="SRV" data-hint="Enter record in the form of PRIORIY WEIGHT PORT TARGET., including trailing period (e.g. 10 10 5060 sip.example.com.).">SRV (service record)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" integrity="sha256-MfvZlkHCEqatNoGiOXveE8FIwMzZg4W85qfrfIFBfYc=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||
<style>
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
@@ -63,7 +63,7 @@
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css" integrity="sha256-bHQiqcFbnJb1Qhh61RY9cMh6kR0gTuQY6iFOBj1yj00=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
</div>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js" integrity="sha256-rsPUGdUPBXgalvIj4YKJrrUlmLXbOb6Cp7cdxn1qeUc=" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js" integrity="sha256-Sk3nkD6mLTMOF0EOpNtsIry+s1CsaqQC1rVLTAy+0yc=" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
var global_modal_state = null;
|
||||
|
||||
@@ -117,7 +117,7 @@ function do_login() {
|
||||
// Open the next panel the user wants to go to. Do this after the XHR response
|
||||
// is over so that we don't start a new XHR request while this one is finishing,
|
||||
// which confuses the loading indicator.
|
||||
setTimeout(function() { show_panel(!switch_back_to_panel ? 'system_status' : switch_back_to_panel) }, 300);
|
||||
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'system_status' : switch_back_to_panel) }, 300);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<h3 id="ssl_install_header">Install Certificate</h3>
|
||||
|
||||
<p>There are many places where you can get a free or cheap certificate. We recommend <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap’s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL’s free express lane</a> or <a href="https://buy.wosign.com/free/">WoSign’s free TLS</a></a>.</p>
|
||||
<p>There are many other places where you can get a free or cheap certificate. If you don't want to use our automatic Let's Encrypt integration, you can give <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap’s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL’s free express lane</a>, <a href="https://buy.wosign.com/free/">WoSign’s free TLS</a></a> or any other certificate provider a try.</p>
|
||||
|
||||
<p>Which domain are you getting a certificate for?</p>
|
||||
|
||||
@@ -250,7 +250,7 @@ function provision_tls_cert() {
|
||||
var now = new Date();
|
||||
n.append(b);
|
||||
function ready_to_finish() {
|
||||
var remaining = r.seconds - Math.round((new Date() - now)/1000);
|
||||
var remaining = Math.round(r.seconds - (new Date() - now)/1000);
|
||||
if (remaining > 0) {
|
||||
setTimeout(ready_to_finish, 1000);
|
||||
b.text("Finish (" + remaining + "...)")
|
||||
|
||||
@@ -142,7 +142,7 @@ function show_system_backup() {
|
||||
var b = r.backups[i];
|
||||
var tr = $('<tr/>');
|
||||
if (b.full) tr.addClass("full-backup");
|
||||
tr.append( $('<td/>').text(b.date_str + " " + r.tz) );
|
||||
tr.append( $('<td/>').text(b.date_str) );
|
||||
tr.append( $('<td/>').text(b.date_delta + " ago") );
|
||||
tr.append( $('<td/>').text(b.full ? "full" : "increment") );
|
||||
tr.append( $('<td style="text-align: right"/>').text( nice_size(b.size)) );
|
||||
|
||||
@@ -34,19 +34,23 @@
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#system-privacy-setting {
|
||||
float: right;
|
||||
max-width: 20em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-push-9 col-md-3">
|
||||
|
||||
<div id="system-reboot-required" style="display: none; margin-bottom: 1em;">
|
||||
<button type="button" class="btn btn-danger" onclick="confirm_reboot(); return false;">Reboot Box</button>
|
||||
<div>No reboot is necessary.</div>
|
||||
</div>
|
||||
|
||||
<div id="system-privacy-setting" style="display: none">
|
||||
<div><a onclick="return enable_privacy(!current_privacy_setting)" href="#"><span>Enable/Disable</span> New-Version Check</a></div>
|
||||
<p style="line-height: 125%"><small>(When enabled, status checks phone-home to check for a new release of Mail-in-a-Box.)</small></p>
|
||||
</div>
|
||||
|
||||
</div> <!-- /col -->
|
||||
<div class="col-md-pull-3 col-md-8">
|
||||
|
||||
<table id="system-checks" class="table" style="max-width: 60em">
|
||||
<thead>
|
||||
@@ -55,6 +59,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div> <!-- /col -->
|
||||
</div> <!-- /row -->
|
||||
|
||||
<script>
|
||||
function show_system_status() {
|
||||
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
|
||||
@@ -70,6 +77,16 @@ function show_system_status() {
|
||||
$('#system-privacy-setting p').toggle(r);
|
||||
});
|
||||
|
||||
api(
|
||||
"/system/reboot",
|
||||
"GET",
|
||||
{ },
|
||||
function(r) {
|
||||
$('#system-reboot-required').show(); // show when r becomes available
|
||||
$('#system-reboot-required').find('button').toggle(r);
|
||||
$('#system-reboot-required').find('div').toggle(!r);
|
||||
});
|
||||
|
||||
api(
|
||||
"/system/status",
|
||||
"POST",
|
||||
@@ -122,4 +139,22 @@ function enable_privacy(status) {
|
||||
});
|
||||
return false; // disable link
|
||||
}
|
||||
|
||||
function confirm_reboot() {
|
||||
show_modal_confirm(
|
||||
"Reboot",
|
||||
$("<p>This will reboot your Mail-in-a-Box <code>{{hostname}}</code>.</p> <p>Until the machine is fully restarted, your users will not be able to send and receive email, and you will not be able to connect to this control panel or with SSH. The reboot cannot be cancelled.</p>"),
|
||||
"Reboot Now",
|
||||
function() {
|
||||
api(
|
||||
"/system/reboot",
|
||||
"POST",
|
||||
{ },
|
||||
function(r) {
|
||||
var msg = "<p>Please reload this page after a minute or so.</p>";
|
||||
if (r) msg = "<p>The reboot command said:</p> <pre>" + $("<pre/>").text(r).html() + "</pre>"; // successful reboots don't produce any output; the output must be HTML-escaped
|
||||
show_modal_error("Reboot", msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -82,7 +82,7 @@ function show_change_web_root(elem) {
|
||||
var root = $(elem).parents('tr').attr('data-custom-web-root');
|
||||
show_modal_confirm(
|
||||
'Change Root Directory for ' + domain,
|
||||
$('<p>You can change the static directory for <tt>' + domain + '</tt> to:</p> <p><tt>' + root + '</tt></p> <p>First create this directory on the server. Then click Update to scan for the directory and update web settings.'),
|
||||
$('<p>You can change the static directory for <tt>' + domain + '</tt> to:</p> <p><tt>' + root + '</tt></p> <p>First create this directory on the server. Then click Update to scan for the directory and update web settings.</p>'),
|
||||
'Update',
|
||||
function() { do_web_update(); });
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
#########################################################
|
||||
# This script is intended to be run like this:
|
||||
#
|
||||
# curl https://.../bootstrap.sh | sudo bash
|
||||
# curl https://mailinabox.email/setup.sh | sudo bash
|
||||
#
|
||||
#########################################################
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
TAG=v0.16
|
||||
TAG=v0.18
|
||||
fi
|
||||
|
||||
# Are we running as root?
|
||||
|
||||
@@ -31,7 +31,7 @@ ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
|
||||
InternalHosts refile:/etc/opendkim/TrustedHosts
|
||||
KeyTable refile:/etc/opendkim/KeyTable
|
||||
SigningTable refile:/etc/opendkim/SigningTable
|
||||
Socket inet:8891@localhost
|
||||
Socket inet:8891@127.0.0.1
|
||||
RequireSafeKeys false
|
||||
EOF
|
||||
fi
|
||||
@@ -39,7 +39,7 @@ fi
|
||||
# Create a new DKIM key. This creates mail.private and mail.txt
|
||||
# in $STORAGE_ROOT/mail/dkim. The former is the private key and
|
||||
# the latter is the suggested DNS TXT entry which we'll include
|
||||
# in our DNS setup. Note tha the files are named after the
|
||||
# in our DNS setup. Note that the files are named after the
|
||||
# 'selector' of the key, which we can change later on to support
|
||||
# key rotation.
|
||||
#
|
||||
|
||||
@@ -57,15 +57,26 @@ apt_install postfix postfix-pcre postgrey ca-certificates
|
||||
# Set some basic settings...
|
||||
#
|
||||
# * Have postfix listen on all network interfaces.
|
||||
# * Make outgoing connections on a particular interface (if multihomed) so that SPF passes on the receiving side.
|
||||
# * Set our name (the Debian default seems to be "localhost" but make it our hostname).
|
||||
# * Set the name of the local machine to localhost, which means xxx@localhost is delivered locally, although we don't use it.
|
||||
# * 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\
|
||||
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
|
||||
mydestination=localhost
|
||||
|
||||
# Tweak some queue settings:
|
||||
# * Inform users when their e-mail delivery is delayed more than 3 hours (default is not to warn).
|
||||
# * Stop trying to send an undeliverable e-mail after 2 days (instead of 5), and for bounce messages just try for 1 day.
|
||||
tools/editconf.py /etc/postfix/main.cf \
|
||||
delay_warning_time=3h \
|
||||
maximal_queue_lifetime=2d \
|
||||
bounce_queue_lifetime=1d
|
||||
|
||||
# ### Outgoing Mail
|
||||
|
||||
# Enable the 'submission' port 587 smtpd server and tweak its settings.
|
||||
|
||||
@@ -38,17 +38,19 @@ passdb {
|
||||
args = /etc/dovecot/dovecot-sql.conf.ext
|
||||
}
|
||||
userdb {
|
||||
driver = static
|
||||
args = uid=mail gid=mail home=$STORAGE_ROOT/mail/mailboxes/%d/%n
|
||||
driver = sql
|
||||
args = /etc/dovecot/dovecot-sql.conf.ext
|
||||
}
|
||||
EOF
|
||||
|
||||
# Configure the SQL to query for a user's password.
|
||||
# Configure the SQL to query for a user's metadata and password.
|
||||
cat > /etc/dovecot/dovecot-sql.conf.ext << EOF;
|
||||
driver = sqlite
|
||||
connect = $db_path
|
||||
default_pass_scheme = SHA512-CRYPT
|
||||
password_query = SELECT email as user, password FROM users WHERE email='%u';
|
||||
user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home FROM users;
|
||||
iterate_query = SELECT email AS user FROM users;
|
||||
EOF
|
||||
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions
|
||||
|
||||
|
||||
@@ -4,19 +4,25 @@ source setup/functions.sh
|
||||
|
||||
echo "Installing Mail-in-a-Box system management daemon..."
|
||||
|
||||
# Switching python 2 boto to package manager's, not pypi's.
|
||||
if [ -f /usr/local/lib/python2.7/dist-packages/boto/__init__.py ]; then hide_output pip uninstall -y boto; fi
|
||||
# Install packages.
|
||||
# flask, yaml, dnspython, and dateutil are all for our Python 3 management daemon itself.
|
||||
# duplicity does backups. python-pip is so we can 'pip install boto' for Python 2, for duplicity, so it can do backups to AWS S3.
|
||||
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil python-pip
|
||||
|
||||
# duplicity uses python 2 so we need to use the python 2 package of boto
|
||||
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
|
||||
apt_install python3-flask links duplicity python-boto libyaml-dev python3-dnspython python3-dateutil \
|
||||
build-essential libssl-dev libffi-dev python3-dev python-pip
|
||||
# These are required to pip install cryptography.
|
||||
apt_install build-essential libssl-dev libffi-dev python3-dev
|
||||
|
||||
# Install other Python packages. The first line is the packages that Josh maintains himself!
|
||||
# Install other Python 3 packages used by the management daemon.
|
||||
# The first line is the packages that Josh maintains himself!
|
||||
# NOTE: email_validator is repeated in setup/questions.sh, so please keep the versions synced.
|
||||
hide_output pip3 install --upgrade \
|
||||
rtyaml "email_validator>=1.0.0" free_tls_certificates \
|
||||
rtyaml "email_validator>=1.0.0" "free_tls_certificates>=0.1.3" \
|
||||
"idna>=2.0.0" "cryptography>=1.0.2" boto psutil
|
||||
# email_validator is repeated in setup/questions.sh
|
||||
|
||||
# duplicity uses python 2 so we need to get the python 2 package of boto to have backups to S3.
|
||||
# boto from the Ubuntu package manager is too out-of-date -- it doesn't support the newer
|
||||
# S3 api used in some regions, which breaks backups to those regions. See #627, #653.
|
||||
hide_output pip install --upgrade boto
|
||||
|
||||
# Create a backup directory and a random key for encrypting backups.
|
||||
mkdir -p $STORAGE_ROOT/backup
|
||||
|
||||
@@ -7,7 +7,8 @@ source /etc/mailinabox.conf # load global vars
|
||||
|
||||
# install Munin
|
||||
echo "Installing Munin (system monitoring)..."
|
||||
apt_install munin munin-node
|
||||
apt_install munin munin-node libcgi-fast-perl
|
||||
# libcgi-fast-perl is needed by /usr/lib/munin/cgi/munin-cgi-graph
|
||||
|
||||
# edit config
|
||||
cat > /etc/munin/munin.conf <<EOF;
|
||||
@@ -19,6 +20,9 @@ tmpldir /etc/munin/templates
|
||||
|
||||
includedir /etc/munin/munin-conf.d
|
||||
|
||||
# path dynazoom uses for requests
|
||||
cgiurl_graph /admin/munin/cgi-graph
|
||||
|
||||
# a simple host tree
|
||||
[$PRIMARY_HOSTNAME]
|
||||
address 127.0.0.1
|
||||
@@ -29,6 +33,10 @@ contact.admin.command mail -s "Munin notification ${var:host}" administrator@$PR
|
||||
contact.admin.always_send warning critical
|
||||
EOF
|
||||
|
||||
# The Debian installer touches these files and chowns them to www-data:adm for use with spawn-fcgi
|
||||
chown munin. /var/log/munin/munin-cgi-html.log
|
||||
chown munin. /var/log/munin/munin-cgi-graph.log
|
||||
|
||||
# ensure munin-node knows the name of this machine
|
||||
tools/editconf.py /etc/munin/munin-node.conf -s \
|
||||
host_name=$PRIMARY_HOSTNAME
|
||||
|
||||
@@ -17,8 +17,8 @@ apt_install \
|
||||
apt-get purge -qq -y owncloud*
|
||||
|
||||
# Install ownCloud from source of this version:
|
||||
owncloud_ver=8.1.1
|
||||
owncloud_hash=34077e78575a3e689825a00964ee37fbf83fbdda
|
||||
owncloud_ver=8.2.3
|
||||
owncloud_hash=bfdf6166fbf6fc5438dc358600e7239d1c970613
|
||||
|
||||
# Migrate <= v0.10 setups that stored the ownCloud config.php in /usr/local rather than
|
||||
# in STORAGE_ROOT. Move the file to STORAGE_ROOT.
|
||||
@@ -52,8 +52,8 @@ if [ ! -d /usr/local/lib/owncloud/ ] \
|
||||
# The two apps we actually want are not in ownCloud core. Clone them from
|
||||
# their github repositories.
|
||||
mkdir -p /usr/local/lib/owncloud/apps
|
||||
git_clone https://github.com/owncloud/contacts 4ff855e7c2075309041bead09fbb9eb7df678244 '' /usr/local/lib/owncloud/apps/contacts
|
||||
git_clone https://github.com/owncloud/calendar ec53139b144c0f842c33813305612e8006c42ea5 '' /usr/local/lib/owncloud/apps/calendar
|
||||
git_clone https://github.com/owncloudarchive/contacts 9ba2e667ae8c7ea36d8c4a4c3413c374beb24b1b '' /usr/local/lib/owncloud/apps/contacts
|
||||
git_clone https://github.com/owncloudarchive/calendar 2086e738a3b7b868ec59cd61f0f88b49c3f21dd1 '' /usr/local/lib/owncloud/apps/calendar
|
||||
|
||||
# Fix weird permissions.
|
||||
chmod 750 /usr/local/lib/owncloud/{apps,config}
|
||||
@@ -108,12 +108,12 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
||||
'user_backends' => array(
|
||||
array(
|
||||
'class'=>'OC_User_IMAP',
|
||||
'arguments'=>array('{localhost:993/imap/ssl/novalidate-cert}')
|
||||
'arguments'=>array('{127.0.0.1:993/imap/ssl/novalidate-cert}')
|
||||
)
|
||||
),
|
||||
'memcache.local' => '\\OC\\Memcache\\Memcached',
|
||||
"memcached_servers" => array (
|
||||
array('localhost', 11211),
|
||||
array('127.0.0.1', 11211),
|
||||
),
|
||||
'mail_smtpmode' => 'sendmail',
|
||||
'mail_smtpsecure' => '',
|
||||
|
||||
@@ -33,3 +33,30 @@ if [ ! -d /vagrant ]; then
|
||||
exit
|
||||
fi
|
||||
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
|
||||
fi
|
||||
|
||||
# Check that no .wgetrc exists
|
||||
if [ -e ~/.wgetrc ]; then
|
||||
echo "Mail-in-a-Box expects no overrides to wget defaults, ~/.wgetrc exists"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check that we are running on x86_64, any other architecture is unsupported and
|
||||
# will fail later in the setup when we try to install the custom build lucene packages.
|
||||
#
|
||||
# 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" ]; then
|
||||
if [ -z "$ARM" ]; then
|
||||
echo "Mail-in-a-Box only supports x86_64 and will not work on any other architecture, like ARM."
|
||||
echo "Your architecture is $ARCHITECTURE"
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -18,7 +18,8 @@ if [ -z "$NONINTERACTIVE" ]; then
|
||||
message_box "Mail-in-a-Box Installation" \
|
||||
"Hello and thanks for deploying a Mail-in-a-Box!
|
||||
\n\nI'm going to ask you a few questions.
|
||||
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line."
|
||||
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line.
|
||||
\n\nNOTE: You should only install this on a brand new Ubuntu installation 100% dedicated to Mail-in-a-Box. Mail-in-a-Box will, for example, remove apache2."
|
||||
fi
|
||||
|
||||
# The box needs a name.
|
||||
|
||||
@@ -78,9 +78,13 @@ tools/editconf.py /etc/spamassassin/local.cf -s \
|
||||
# * Writable 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
|
||||
# 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=0660
|
||||
|
||||
mkdir -p $STORAGE_ROOT/mail/spamassassin
|
||||
chown -R spampd:spampd $STORAGE_ROOT/mail/spamassassin
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
source setup/functions.sh # load our functions
|
||||
|
||||
# Check system setup: Are we running as root on Ubuntu 14.04 on a
|
||||
# machine with enough memory? If not, this shows an error and exits.
|
||||
# machine with enough memory? Is /tmp mounted with exec.
|
||||
# If not, this shows an error and exits.
|
||||
source setup/preflight.sh
|
||||
|
||||
# Ensure Python reads/writes files in UTF-8. If the machine
|
||||
# triggers some other locale in Python, like ASCII encoding,
|
||||
# Python may not be able to read/write files. Here and in
|
||||
# the management daemon startup script.
|
||||
# Python may not be able to read/write files. This is also
|
||||
# in the management daemon startup script and the cron script.
|
||||
|
||||
if [ -z `locale -a | grep en_US.utf8` ]; then
|
||||
# Generate locale if not exists
|
||||
@@ -23,6 +24,9 @@ export LC_ALL=en_US.UTF-8
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_TYPE=en_US.UTF-8
|
||||
|
||||
# Fix so line drawing characters are shown correctly in Putty on Windows. See #744.
|
||||
export NCURSES_NO_UTF8_ACS=1
|
||||
|
||||
# Recall the last settings used if we're running this a second time.
|
||||
if [ -f /etc/mailinabox.conf ]; then
|
||||
# Run any system migrations before proceeding. Since this is a second run,
|
||||
@@ -108,7 +112,7 @@ source setup/management.sh
|
||||
source setup/munin.sh
|
||||
|
||||
# Ping the management daemon to write the DNS and nginx configuration files.
|
||||
until nc -z -w 4 localhost 10222
|
||||
until nc -z -w 4 127.0.0.1 10222
|
||||
do
|
||||
echo Waiting for the Mail-in-a-Box management daemon to start...
|
||||
sleep 2
|
||||
|
||||
@@ -4,6 +4,70 @@ source setup/functions.sh # load our functions
|
||||
# Basic System Configuration
|
||||
# -------------------------
|
||||
|
||||
# ### Set hostname of the box
|
||||
|
||||
# If the hostname is not correctly resolvable sudo can't be used. This will result in
|
||||
# errors during the install
|
||||
#
|
||||
# First set the hostname in the configuration file, then activate the setting
|
||||
|
||||
echo $PRIMARY_HOSTNAME > /etc/hostname
|
||||
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
|
||||
# prevent for instance spam filtering from crashing
|
||||
|
||||
# We will create a 1G file, this should be a good balance between disk usage
|
||||
# and buffers for the system. We will only allocate this file if there is more
|
||||
# than 5GB of disk space available
|
||||
|
||||
# The following checks are performed:
|
||||
# - Check if swap is currently mountend by looking at /proc/swaps
|
||||
# - Check if the user intents to activate swap on next boot by checking fstab entries.
|
||||
# - Check if a swapfile already exists
|
||||
# - Check if the root file system is not btrfs, might be an incompatible version with
|
||||
# swapfiles. User should hanle it them selves.
|
||||
# - Check the memory requirements
|
||||
# - Check available diskspace
|
||||
|
||||
# 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_IN_FSTAB=$(grep "swap" /etc/fstab)
|
||||
ROOT_IS_BTRFS=$(grep "\/ .*btrfs" /proc/mounts)
|
||||
TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}')
|
||||
AVAILABLE_DISK_SPACE=$(df / --output=avail | tail -n 1)
|
||||
if
|
||||
[ -z "$SWAP_MOUNTED" ] &&
|
||||
[ -z "$SWAP_IN_FSTAB" ] &&
|
||||
[ ! -e /swapfile ] &&
|
||||
[ -z "$ROOT_IS_BTRFS" ] &&
|
||||
[ $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
|
||||
# doing it in one go, could fail on low memory systems
|
||||
dd if=/dev/zero of=/swapfile bs=1024 count=$[1024*1024] status=none
|
||||
if [ -e /swapfile ]; then
|
||||
chmod 600 /swapfile
|
||||
hide_output mkswap /swapfile
|
||||
swapon /swapfile
|
||||
fi
|
||||
|
||||
# Check if swap is mounted then activate on boot
|
||||
if swapon -s | grep -q "\/swapfile"; then
|
||||
echo "/swapfile none swap sw 0 0" >> /etc/fstab
|
||||
else
|
||||
echo "ERROR: Swap allocation failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ### Add Mail-in-a-Box's PPA.
|
||||
|
||||
# We've built several .deb packages on our own that we want to include.
|
||||
|
||||
@@ -34,11 +34,11 @@ apt-get purge -qq -y roundcube* #NODOC
|
||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
||||
# Combine the Roundcube version number with the commit hash of vacation_sieve to track
|
||||
# whether we have the latest version.
|
||||
VERSION=1.1.3
|
||||
HASH=4513227bd64eb8564f056817341b1dfe478e215e
|
||||
VERSION=1.1.5
|
||||
HASH=8A59D196EF0AA6D9C717B00699215135ABCB99CF
|
||||
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
|
||||
PERSISTENT_LOGIN_VERSION=117fbd8f93b56b2bf72ad055193464803ef3bc36
|
||||
HTML5_NOTIFIER_VERSION=046eb388dd63b1ec77a3ee485757fc25ae9e684d
|
||||
PERSISTENT_LOGIN_VERSION=1e9d724476a370ce917a2fcd5b3217b0c306c24e
|
||||
HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5
|
||||
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:a
|
||||
needs_update=0 #NODOC
|
||||
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
|
||||
@@ -51,7 +51,7 @@ fi
|
||||
if [ $needs_update == 1 ]; then
|
||||
# install roundcube
|
||||
wget_verify \
|
||||
https://downloads.sourceforge.net/project/roundcubemail/roundcubemail/$VERSION/roundcubemail-$VERSION.tar.gz \
|
||||
https://github.com/roundcube/roundcubemail/releases/download/$VERSION/roundcubemail-$VERSION.tar.gz \
|
||||
$HASH \
|
||||
/tmp/roundcube.tgz
|
||||
tar -C /usr/local/lib --no-same-owner -zxf /tmp/roundcube.tgz
|
||||
@@ -91,15 +91,15 @@ cat > /usr/local/lib/roundcubemail/config/config.inc.php <<EOF;
|
||||
\$config['log_dir'] = '/var/log/roundcubemail/';
|
||||
\$config['temp_dir'] = '/tmp/roundcubemail/';
|
||||
\$config['db_dsnw'] = 'sqlite:///$STORAGE_ROOT/mail/roundcube/roundcube.sqlite?mode=0640';
|
||||
\$config['default_host'] = 'ssl://localhost';
|
||||
\$config['default_host'] = 'ssl://127.0.0.1';
|
||||
\$config['default_port'] = 993;
|
||||
\$config['imap_timeout'] = 15;
|
||||
\$config['smtp_server'] = 'tls://localhost';
|
||||
\$config['smtp_server'] = 'tls://127.0.0.1';
|
||||
\$config['smtp_port'] = 587;
|
||||
\$config['smtp_user'] = '%u';
|
||||
\$config['smtp_pass'] = '%p';
|
||||
\$config['support_url'] = 'https://mailinabox.email/';
|
||||
\$config['product_name'] = 'Mail-in-a-Box/Roundcube Webmail';
|
||||
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
|
||||
\$config['des_key'] = '$SECRET_KEY';
|
||||
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'vacation_sieve', 'persistent_login');
|
||||
\$config['skin'] = 'classic';
|
||||
@@ -121,7 +121,7 @@ cat > /usr/local/lib/roundcubemail/plugins/vacation_sieve/config.inc.php <<EOF;
|
||||
'transfer' => array(
|
||||
'mode' => 'managesieve',
|
||||
'ms_activate_script' => true,
|
||||
'host' => 'localhost',
|
||||
'host' => '127.0.0.1',
|
||||
'port' => '4190',
|
||||
'usetls' => false,
|
||||
'path' => 'vacation',
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
#
|
||||
# This is a tool Josh uses on his box serving mailinabox.email to parse the nginx
|
||||
# access log to see how many people are installing Mail-in-a-Box each day, by
|
||||
# looking at accesses to the bootstrap.sh script.
|
||||
# looking at accesses to the bootstrap.sh script (which is currently at the URL
|
||||
# .../setup.sh).
|
||||
|
||||
import re, glob, gzip, os.path, json
|
||||
import dateutil.parser
|
||||
@@ -24,9 +25,10 @@ for fn in glob.glob("/var/log/nginx/access.log*"):
|
||||
# Loop through the lines in the access log.
|
||||
with f:
|
||||
for line in f:
|
||||
# Find lines that are GETs on /bootstrap.sh by either curl or wget.
|
||||
# Find lines that are GETs on the bootstrap script by either curl or wget.
|
||||
# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
|
||||
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /bootstrap.sh HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
|
||||
# (Also, the URL changed in January 2016, but we'll accept both.)
|
||||
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /(bootstrap.sh|setup.sh) HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
|
||||
if m:
|
||||
date, time = m.group("date").decode("ascii").split(":", 1)
|
||||
date = dateutil.parser.parse(date).date().isoformat()
|
||||
|
||||
Reference in New Issue
Block a user