bringing in quota changes
This commit is contained in:
parent
830c83daa1
commit
1795f8aefd
|
@ -0,0 +1,63 @@
|
||||||
|
## NOTE: This file is automatically generated by Mail-in-a-Box.
|
||||||
|
## Do not edit this file. It is continually updated by
|
||||||
|
## Mail-in-a-Box and your changes will be lost.
|
||||||
|
##
|
||||||
|
## Mail-in-a-Box machines are not meant to be modified.
|
||||||
|
## If you modify any system configuration you are on
|
||||||
|
## your own --- please do not ask for help from us.
|
||||||
|
|
||||||
|
namespace inbox {
|
||||||
|
# Automatically create & subscribe some folders.
|
||||||
|
# * Create and subscribe the INBOX folder.
|
||||||
|
# * Our sieve rule for spam expects that the Spam folder exists.
|
||||||
|
# * Z-Push must be configured with the same settings in conf/zpush/backend_imap.php (#580).
|
||||||
|
|
||||||
|
# MUA notes:
|
||||||
|
# * Roundcube will show an error if the user tries to delete a message before the Trash folder exists (#359).
|
||||||
|
# * K-9 mail will poll every 90 seconds if a Drafts folder does not exist.
|
||||||
|
# * Apple's OS X Mail app will create 'Sent Messages' if it doesn't see a folder with the \Sent flag (#571, #573) and won't be able to archive messages unless 'Archive' exists (#581).
|
||||||
|
# * Thunderbird's default in its UI is 'Archives' (plural) but it will configure new accounts to use whatever we say here (#581).
|
||||||
|
|
||||||
|
# auto:
|
||||||
|
# 'create' will automatically create this mailbox.
|
||||||
|
# 'subscribe' will both create and subscribe to the mailbox.
|
||||||
|
|
||||||
|
# special_use is a space separated list of IMAP SPECIAL-USE
|
||||||
|
# attributes as specified by RFC 6154:
|
||||||
|
# \All \Archive \Drafts \Flagged \Junk \Sent \Trash
|
||||||
|
|
||||||
|
mailbox INBOX {
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Spam {
|
||||||
|
special_use = \Junk
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Drafts {
|
||||||
|
special_use = \Drafts
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Sent {
|
||||||
|
special_use = \Sent
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Trash {
|
||||||
|
special_use = \Trash
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Archive {
|
||||||
|
special_use = \Archive
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
|
||||||
|
# dovevot's standard mailboxes configuration file marks two sent folders
|
||||||
|
# with the \Sent attribute, just in case clients don't agree about which
|
||||||
|
# they're using. We'll keep that, plus add Junk as an alterative for Spam.
|
||||||
|
# These are not auto-created.
|
||||||
|
mailbox "Sent Messages" {
|
||||||
|
special_use = \Sent
|
||||||
|
}
|
||||||
|
mailbox Junk {
|
||||||
|
special_use = \Junk
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,11 +60,13 @@ def setup_key_auth(mgmt_uri):
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("""Usage:
|
print("""Usage:
|
||||||
|
{cli} system default-quota [new default] (set default quota for system)
|
||||||
{cli} user (lists users)
|
{cli} user (lists users)
|
||||||
{cli} user add user@domain.com [password]
|
{cli} user add user@domain.com [password]
|
||||||
{cli} user password user@domain.com [password]
|
{cli} user password user@domain.com [password]
|
||||||
{cli} user remove user@domain.com
|
{cli} user remove user@domain.com
|
||||||
{cli} user make-admin user@domain.com
|
{cli} user make-admin user@domain.com
|
||||||
|
{cli} user quota user@domain [new-quota]
|
||||||
{cli} user remove-admin user@domain.com
|
{cli} user remove-admin user@domain.com
|
||||||
{cli} user admins (lists admins)
|
{cli} user admins (lists admins)
|
||||||
{cli} user mfa show user@domain.com (shows MFA devices for user, if any)
|
{cli} user mfa show user@domain.com (shows MFA devices for user, if any)
|
||||||
|
@ -88,6 +90,10 @@ elif sys.argv[1] == "user" and len(sys.argv) == 2:
|
||||||
print(user['email'], end='')
|
print(user['email'], end='')
|
||||||
if "admin" in user['privileges']:
|
if "admin" in user['privileges']:
|
||||||
print("*", end='')
|
print("*", end='')
|
||||||
|
if user['quota'] == '0':
|
||||||
|
print(" unlimited", end='')
|
||||||
|
else:
|
||||||
|
print(" " + user['quota'], end='')
|
||||||
print()
|
print()
|
||||||
|
|
||||||
elif sys.argv[1] == "user" and sys.argv[2] in {"add", "password"}:
|
elif sys.argv[1] == "user" and sys.argv[2] in {"add", "password"}:
|
||||||
|
@ -117,6 +123,14 @@ elif sys.argv[1] == "user" and sys.argv[2] == "admins":
|
||||||
if "admin" in user['privileges']:
|
if "admin" in user['privileges']:
|
||||||
print(user['email'])
|
print(user['email'])
|
||||||
|
|
||||||
|
elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 4:
|
||||||
|
# Set a user's quota
|
||||||
|
print(mgmt("/mail/users/quota?text=1&email=%s" % sys.argv[3]))
|
||||||
|
|
||||||
|
elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 5:
|
||||||
|
# Set a user's quota
|
||||||
|
users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] })
|
||||||
|
|
||||||
elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]:
|
elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]:
|
||||||
# Show MFA status for a user.
|
# Show MFA status for a user.
|
||||||
status = mgmt("/mfa/status", { "user": sys.argv[4] }, is_json=True)
|
status = mgmt("/mfa/status", { "user": sys.argv[4] }, is_json=True)
|
||||||
|
@ -138,6 +152,12 @@ elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5:
|
||||||
elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
|
elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
|
||||||
print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] }))
|
print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] }))
|
||||||
|
|
||||||
|
elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 3:
|
||||||
|
print(mgmt("/system/default-quota?text=1"))
|
||||||
|
|
||||||
|
elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 4:
|
||||||
|
print(mgmt("/system/default-quota", { "default_quota": sys.argv[3]}))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("Invalid command-line arguments.")
|
print("Invalid command-line arguments.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import auth, utils
|
||||||
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_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_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
|
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
|
||||||
|
from mailconfig import get_mail_quota, set_mail_quota, get_default_quota, validate_quota
|
||||||
from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa
|
from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
|
@ -191,8 +192,31 @@ def mail_users():
|
||||||
@app.route('/mail/users/add', methods=['POST'])
|
@app.route('/mail/users/add', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def mail_users_add():
|
def mail_users_add():
|
||||||
|
quota = request.form.get('quota', get_default_quota(env))
|
||||||
try:
|
try:
|
||||||
return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), env)
|
return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), quota, env)
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
|
||||||
|
@app.route('/mail/users/quota', methods=['GET'])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def get_mail_users_quota():
|
||||||
|
email = request.values.get('email', '')
|
||||||
|
quota = get_mail_quota(email, env)
|
||||||
|
|
||||||
|
if request.values.get('text'):
|
||||||
|
return quota
|
||||||
|
|
||||||
|
return json_response({
|
||||||
|
"email": email,
|
||||||
|
"quota": quota
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/mail/users/quota', methods=['POST'])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def mail_users_quota():
|
||||||
|
try:
|
||||||
|
return set_mail_quota(request.form.get('email', ''), request.form.get('quota'), env)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return (str(e), 400)
|
return (str(e), 400)
|
||||||
|
|
||||||
|
@ -651,6 +675,29 @@ def privacy_status_set():
|
||||||
utils.write_settings(config, env)
|
utils.write_settings(config, env)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
@app.route('/system/default-quota', methods=["GET"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def default_quota_get():
|
||||||
|
if request.values.get('text'):
|
||||||
|
return get_default_quota(env)
|
||||||
|
else:
|
||||||
|
return json_response({
|
||||||
|
"default-quota": get_default_quota(env),
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/system/default-quota', methods=["POST"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def default_quota_set():
|
||||||
|
config = utils.load_settings(env)
|
||||||
|
try:
|
||||||
|
config["default-quota"] = validate_quota(request.values.get('default_quota'))
|
||||||
|
utils.write_settings(config, env)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return ("ERROR: %s" % str(e), 400)
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
|
||||||
# MUNIN
|
# MUNIN
|
||||||
|
|
||||||
@app.route('/munin/')
|
@app.route('/munin/')
|
||||||
|
|
|
@ -102,6 +102,18 @@ def get_mail_users(env):
|
||||||
users = [ row[0] for row in c.fetchall() ]
|
users = [ row[0] for row in c.fetchall() ]
|
||||||
return utils.sort_email_addresses(users, env)
|
return utils.sort_email_addresses(users, env)
|
||||||
|
|
||||||
|
def sizeof_fmt(num):
|
||||||
|
for unit in ['','K','M','G','T']:
|
||||||
|
if abs(num) < 1024.0:
|
||||||
|
if abs(num) > 99:
|
||||||
|
return "%3.0f%s" % (num, unit)
|
||||||
|
else:
|
||||||
|
return "%2.1f%s" % (num, unit)
|
||||||
|
|
||||||
|
num /= 1024.0
|
||||||
|
|
||||||
|
return str(num)
|
||||||
|
|
||||||
def get_mail_users_ex(env, with_archived=False):
|
def get_mail_users_ex(env, with_archived=False):
|
||||||
# Returns a complex data structure of all user accounts, optionally
|
# Returns a complex data structure of all user accounts, optionally
|
||||||
# including archived (status="inactive") accounts.
|
# including archived (status="inactive") accounts.
|
||||||
|
@ -125,13 +137,46 @@ def get_mail_users_ex(env, with_archived=False):
|
||||||
users = []
|
users = []
|
||||||
active_accounts = set()
|
active_accounts = set()
|
||||||
c = open_database(env)
|
c = open_database(env)
|
||||||
c.execute('SELECT email, privileges FROM users')
|
c.execute('SELECT email, privileges, quota FROM users')
|
||||||
for email, privileges in c.fetchall():
|
for email, privileges, quota in c.fetchall():
|
||||||
active_accounts.add(email)
|
active_accounts.add(email)
|
||||||
|
|
||||||
|
(user, domain) = email.split('@')
|
||||||
|
box_size = 0
|
||||||
|
box_count = 0
|
||||||
|
box_quota = 0
|
||||||
|
percent = ''
|
||||||
|
try:
|
||||||
|
dirsize_file = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes/%s/%s/maildirsize' % (domain, user))
|
||||||
|
with open(dirsize_file, 'r') as f:
|
||||||
|
box_quota = int(f.readline().split('S')[0])
|
||||||
|
for line in f.readlines():
|
||||||
|
(size, count) = line.split(' ')
|
||||||
|
box_size += int(size)
|
||||||
|
box_count += int(count)
|
||||||
|
|
||||||
|
try:
|
||||||
|
percent = (box_size / box_quota) * 100
|
||||||
|
except:
|
||||||
|
percent = 'Error'
|
||||||
|
|
||||||
|
except:
|
||||||
|
box_size = '?'
|
||||||
|
box_count = '?'
|
||||||
|
box_quota = '?'
|
||||||
|
percent = '?'
|
||||||
|
|
||||||
|
if quota == '0':
|
||||||
|
percent = ''
|
||||||
|
|
||||||
user = {
|
user = {
|
||||||
"email": email,
|
"email": email,
|
||||||
"privileges": parse_privs(privileges),
|
"privileges": parse_privs(privileges),
|
||||||
|
"quota": quota,
|
||||||
|
"box_quota": box_quota,
|
||||||
|
"box_size": sizeof_fmt(box_size) if box_size != '?' else box_size,
|
||||||
|
"percent": '%3.0f%%' % percent if type(percent) != str else percent,
|
||||||
|
"box_count": box_count,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
}
|
}
|
||||||
users.append(user)
|
users.append(user)
|
||||||
|
@ -150,6 +195,10 @@ def get_mail_users_ex(env, with_archived=False):
|
||||||
"privileges": [],
|
"privileges": [],
|
||||||
"status": "inactive",
|
"status": "inactive",
|
||||||
"mailbox": mbox,
|
"mailbox": mbox,
|
||||||
|
"box_count": '?',
|
||||||
|
"box_size": '?',
|
||||||
|
"box_quota": '?',
|
||||||
|
"percent": '?',
|
||||||
}
|
}
|
||||||
users.append(user)
|
users.append(user)
|
||||||
|
|
||||||
|
@ -266,7 +315,7 @@ def get_mail_domains(env, filter_aliases=lambda alias : True, users_only=False):
|
||||||
domains.extend([get_domain(address, as_unicode=False) for address, _, _, auto in get_mail_aliases(env) if filter_aliases(address) and not auto ])
|
domains.extend([get_domain(address, as_unicode=False) for address, _, _, auto in get_mail_aliases(env) if filter_aliases(address) and not auto ])
|
||||||
return set(domains)
|
return set(domains)
|
||||||
|
|
||||||
def add_mail_user(email, pw, privs, env):
|
def add_mail_user(email, pw, privs, quota, env):
|
||||||
# validate email
|
# validate email
|
||||||
if email.strip() == "":
|
if email.strip() == "":
|
||||||
return ("No email address provided.", 400)
|
return ("No email address provided.", 400)
|
||||||
|
@ -292,6 +341,14 @@ def add_mail_user(email, pw, privs, env):
|
||||||
validation = validate_privilege(p)
|
validation = validate_privilege(p)
|
||||||
if validation: return validation
|
if validation: return validation
|
||||||
|
|
||||||
|
if quota is None:
|
||||||
|
quota = get_default_quota()
|
||||||
|
|
||||||
|
try:
|
||||||
|
quota = validate_quota(quota)
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
|
||||||
# get the database
|
# get the database
|
||||||
conn, c = open_database(env, with_connection=True)
|
conn, c = open_database(env, with_connection=True)
|
||||||
|
|
||||||
|
@ -300,14 +357,16 @@ def add_mail_user(email, pw, privs, env):
|
||||||
|
|
||||||
# add the user to the database
|
# add the user to the database
|
||||||
try:
|
try:
|
||||||
c.execute("INSERT INTO users (email, password, privileges) VALUES (?, ?, ?)",
|
c.execute("INSERT INTO users (email, password, privileges, quota) VALUES (?, ?, ?, ?)",
|
||||||
(email, pw, "\n".join(privs)))
|
(email, pw, "\n".join(privs), quota))
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
return ("User already exists.", 400)
|
return ("User already exists.", 400)
|
||||||
|
|
||||||
# write databasebefore next step
|
# write databasebefore next step
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
dovecot_quota_recalc(email)
|
||||||
|
|
||||||
# Update things in case any new domains are added.
|
# Update things in case any new domains are added.
|
||||||
return kick(env, "mail user added")
|
return kick(env, "mail user added")
|
||||||
|
|
||||||
|
@ -332,6 +391,59 @@ def hash_password(pw):
|
||||||
# http://wiki2.dovecot.org/Authentication/PasswordSchemes
|
# http://wiki2.dovecot.org/Authentication/PasswordSchemes
|
||||||
return utils.shell('check_output', ["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip()
|
return utils.shell('check_output', ["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_mail_quota(email, env):
|
||||||
|
conn, c = open_database(env, with_connection=True)
|
||||||
|
c.execute("SELECT quota FROM users WHERE email=?", (email,))
|
||||||
|
rows = c.fetchall()
|
||||||
|
if len(rows) != 1:
|
||||||
|
return ("That's not a user (%s)." % email, 400)
|
||||||
|
|
||||||
|
return rows[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def set_mail_quota(email, quota, env):
|
||||||
|
# validate that password is acceptable
|
||||||
|
quota = validate_quota(quota)
|
||||||
|
|
||||||
|
# update the database
|
||||||
|
conn, c = open_database(env, with_connection=True)
|
||||||
|
c.execute("UPDATE users SET quota=? WHERE email=?", (quota, email))
|
||||||
|
if c.rowcount != 1:
|
||||||
|
return ("That's not a user (%s)." % email, 400)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
dovecot_quota_recalc(email)
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
def dovecot_quota_recalc(email):
|
||||||
|
# dovecot processes running for the user will not recognize the new quota setting
|
||||||
|
# a reload is necessary to reread the quota setting, but it will also shut down
|
||||||
|
# running dovecot processes. Email clients generally log back in when they lose
|
||||||
|
# a connection.
|
||||||
|
# subprocess.call(['doveadm', 'reload'])
|
||||||
|
|
||||||
|
# force dovecot to recalculate the quota info for the user.
|
||||||
|
subprocess.call(["doveadm", "quota", "recalc", "-u", email])
|
||||||
|
|
||||||
|
def get_default_quota(env):
|
||||||
|
config = utils.load_settings(env)
|
||||||
|
return config.get("default-quota", '0')
|
||||||
|
|
||||||
|
def validate_quota(quota):
|
||||||
|
# validate quota
|
||||||
|
quota = quota.strip().upper()
|
||||||
|
|
||||||
|
if quota == "":
|
||||||
|
raise ValueError("No quota provided.")
|
||||||
|
if re.search(r"[\s,.]", quota):
|
||||||
|
raise ValueError("Quotas cannot contain spaces, commas, or decimal points.")
|
||||||
|
if not re.match(r'^[\d]+[GM]?$', quota):
|
||||||
|
raise ValueError("Invalid quota.")
|
||||||
|
|
||||||
|
return quota
|
||||||
|
|
||||||
def get_mail_password(email, env):
|
def get_mail_password(email, env):
|
||||||
# Gets the hashed password for a user. Passwords are stored in Dovecot's
|
# Gets the hashed password for a user. Passwords are stored in Dovecot's
|
||||||
# password format, with a prefixed scheme.
|
# password format, with a prefixed scheme.
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#user_table .account_inactive .if_active { display: none; }
|
#user_table .account_inactive .if_active { display: none; }
|
||||||
#user_table .account_active .if_inactive { display: none; }
|
#user_table .account_active .if_inactive { display: none; }
|
||||||
#user_table .account_active.if_inactive { display: none; }
|
#user_table .account_active.if_inactive { display: none; }
|
||||||
|
.row-center { text-align: center; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<h3>Add a mail user</h3>
|
<h3>Add a mail user</h3>
|
||||||
|
@ -27,6 +28,10 @@
|
||||||
<option value="admin">Administrator</option>
|
<option value="admin">Administrator</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="sr-only" for="adduserQuota">Quota</label>
|
||||||
|
<input type="text" class="form-control" id="adduserQuota" placeholder="Quota" style="width:5em;">
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Add User</button>
|
<button type="submit" class="btn btn-primary">Add User</button>
|
||||||
</form>
|
</form>
|
||||||
<ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;">
|
<ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;">
|
||||||
|
@ -34,13 +39,18 @@
|
||||||
<li>Use <a href="#aliases">aliases</a> to create email addresses that forward to existing accounts.</li>
|
<li>Use <a href="#aliases">aliases</a> to create email addresses that forward to existing accounts.</li>
|
||||||
<li>Administrators get access to this control panel.</li>
|
<li>Administrators get access to this control panel.</li>
|
||||||
<li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#aliases">aliases</a> can.</li>
|
<li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#aliases">aliases</a> can.</li>
|
||||||
|
<li>Quotas may not contain any spaces, commas or decimal points. Suffixes of G (gigabytes) and M (megabytes) are allowed. For unlimited storage enter 0 (zero)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Existing mail users</h3>
|
<h3>Existing mail users</h3>
|
||||||
<table id="user_table" class="table" style="width: auto">
|
<table id="user_table" class="table" style="width: auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th width="50%">Email Address</th>
|
<th width="35%">Email Address</th>
|
||||||
|
<th class="row-center">Messages</th>
|
||||||
|
<th class="row-center">Size</th>
|
||||||
|
<th class="row-center">Used</th>
|
||||||
|
<th class="row-center">Quota</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -53,10 +63,22 @@
|
||||||
<tr id="user-template">
|
<tr id="user-template">
|
||||||
<td class='address'>
|
<td class='address'>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="box-count row-center"></td>
|
||||||
|
<td class="box-size row-center"></td>
|
||||||
|
<td class="percent row-center"></td>
|
||||||
|
<td class="quota row-center">
|
||||||
|
</td>
|
||||||
<td class='actions'>
|
<td class='actions'>
|
||||||
<span class='privs'>
|
<span class='privs'>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span class="if_active">
|
||||||
|
<a href="#" onclick="users_set_quota(this); return false;" class='setquota' title="Set Quota">
|
||||||
|
set quota
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="if_active">
|
<span class="if_active">
|
||||||
<a href="#" onclick="users_set_password(this); return false;" class='setpw' title="Set Password">
|
<a href="#" onclick="users_set_password(this); return false;" class='setpw' title="Set Password">
|
||||||
set password
|
set password
|
||||||
|
@ -97,10 +119,28 @@
|
||||||
<table class="table" style="margin-top: .5em">
|
<table class="table" style="margin-top: .5em">
|
||||||
<thead><th>Verb</th> <th>Action</th><th></th></thead>
|
<thead><th>Verb</th> <th>Action</th><th></th></thead>
|
||||||
<tr><td>GET</td><td><i>(none)</i></td> <td>Returns a list of existing mail users. Adding <code>?format=json</code> to the URL will give JSON-encoded results.</td></tr>
|
<tr><td>GET</td><td><i>(none)</i></td> <td>Returns a list of existing mail users. Adding <code>?format=json</code> to the URL will give JSON-encoded results.</td></tr>
|
||||||
<tr><td>POST</td><td>/add</td> <td>Adds a new mail user. Required POST-body parameters are <code>email</code> and <code>password</code>.</td></tr>
|
<tr>
|
||||||
<tr><td>POST</td><td>/remove</td> <td>Removes a mail user. Required POST-body parameter is <code>email</code>.</td></tr>
|
<td>POST</td>
|
||||||
|
<td>/add</td>
|
||||||
|
<td>Adds a new mail user. Required POST-body parameters are <code>email</code> and <code>password</code>. Optional parameters: <code>privilege=admin</code> and <code>quota</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>POST</td>
|
||||||
|
<td>/remove</td>
|
||||||
|
<td>Removes a mail user. Required POST-by parameter is <code>email</code>.</td>
|
||||||
|
</tr>
|
||||||
<tr><td>POST</td><td>/privileges/add</td> <td>Used to make a mail user an admin. Required POST-body parameters are <code>email</code> and <code>privilege=admin</code>.</td></tr>
|
<tr><td>POST</td><td>/privileges/add</td> <td>Used to make a mail user an admin. Required POST-body parameters are <code>email</code> and <code>privilege=admin</code>.</td></tr>
|
||||||
<tr><td>POST</td><td>/privileges/remove</td> <td>Used to remove the admin privilege from a mail user. Required POST-body parameter is <code>email</code>.</td></tr>
|
<tr><td>POST</td><td>/privileges/remove</td> <td>Used to remove the admin privilege from a mail user. Required POST-body parameter is <code>email</code>.</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>GET</td>
|
||||||
|
<td>/quota</td>
|
||||||
|
<td>Get the quota for a mail user. Required POST-body parameters are <code>email</code> and will return JSON result</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>POST</td>
|
||||||
|
<td>/quota</td>
|
||||||
|
<td>Set the quota for a mail user. Required POST-body parameters are <code>email</code> and <code>quota</code>.</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h4>Examples:</h4>
|
<h4>Examples:</h4>
|
||||||
|
@ -125,6 +165,15 @@ curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/us
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function show_users() {
|
function show_users() {
|
||||||
|
api(
|
||||||
|
"/system/default-quota",
|
||||||
|
"GET",
|
||||||
|
{},
|
||||||
|
function(r) {
|
||||||
|
$('#adduserQuota').val(r['default-quota']);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
$('#user_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
|
$('#user_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
|
||||||
api(
|
api(
|
||||||
"/mail/users",
|
"/mail/users",
|
||||||
|
@ -133,7 +182,7 @@ function show_users() {
|
||||||
function(r) {
|
function(r) {
|
||||||
$('#user_table tbody').html("");
|
$('#user_table tbody').html("");
|
||||||
for (var i = 0; i < r.length; i++) {
|
for (var i = 0; i < r.length; i++) {
|
||||||
var hdr = $("<tr><th colspan='2' style='background-color: #EEE'></th></tr>");
|
var hdr = $("<tr><th colspan='6' style='background-color: #EEE'></th></tr>");
|
||||||
hdr.find('th').text(r[i].domain);
|
hdr.find('th').text(r[i].domain);
|
||||||
$('#user_table tbody').append(hdr);
|
$('#user_table tbody').append(hdr);
|
||||||
|
|
||||||
|
@ -151,7 +200,18 @@ function show_users() {
|
||||||
n2.addClass("account_" + user.status);
|
n2.addClass("account_" + user.status);
|
||||||
|
|
||||||
n.attr('data-email', user.email);
|
n.attr('data-email', user.email);
|
||||||
n.find('.address').text(user.email)
|
n.attr('data-quota', user.quota);
|
||||||
|
n.find('.address').text(user.email);
|
||||||
|
n.find('.box-count').text((user.box_count).toLocaleString('en'));
|
||||||
|
if (user.box_count == '?') {
|
||||||
|
n.find('.box-count').attr('title', 'Message count is unkown')
|
||||||
|
}
|
||||||
|
n.find('.box-size').text(user.box_size);
|
||||||
|
if (user.box_size == '?') {
|
||||||
|
n.find('.box-size').attr('title', 'Mailbox size is unkown')
|
||||||
|
}
|
||||||
|
n.find('.percent').text(user.percent);
|
||||||
|
n.find('.quota').text((user.quota == '0') ? 'unlimited' : user.quota);
|
||||||
n2.find('.restore_info tt').text(user.mailbox);
|
n2.find('.restore_info tt').text(user.mailbox);
|
||||||
|
|
||||||
if (user.status == 'inactive') continue;
|
if (user.status == 'inactive') continue;
|
||||||
|
@ -180,13 +240,15 @@ function do_add_user() {
|
||||||
var email = $("#adduserEmail").val();
|
var email = $("#adduserEmail").val();
|
||||||
var pw = $("#adduserPassword").val();
|
var pw = $("#adduserPassword").val();
|
||||||
var privs = $("#adduserPrivs").val();
|
var privs = $("#adduserPrivs").val();
|
||||||
|
var quota = $("#adduserQuota").val();
|
||||||
api(
|
api(
|
||||||
"/mail/users/add",
|
"/mail/users/add",
|
||||||
"POST",
|
"POST",
|
||||||
{
|
{
|
||||||
email: email,
|
email: email,
|
||||||
password: pw,
|
password: pw,
|
||||||
privileges: privs
|
privileges: privs,
|
||||||
|
quota: quota
|
||||||
},
|
},
|
||||||
function(r) {
|
function(r) {
|
||||||
// Responses are multiple lines of pre-formatted text.
|
// Responses are multiple lines of pre-formatted text.
|
||||||
|
@ -228,6 +290,36 @@ function users_set_password(elem) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function users_set_quota(elem) {
|
||||||
|
var email = $(elem).parents('tr').attr('data-email');
|
||||||
|
var quota = $(elem).parents('tr').attr('data-quota');
|
||||||
|
|
||||||
|
show_modal_confirm(
|
||||||
|
"Set Quota",
|
||||||
|
$("<p>Set quota for <b>" + email + "</b>?</p>" +
|
||||||
|
"<p>" +
|
||||||
|
"<label for='users_set_quota' style='display: block; font-weight: normal'>Quota:</label>" +
|
||||||
|
"<input type='text' id='users_set_quota' value='" + quota + "'></p>" +
|
||||||
|
"<p><small>Quotas may not contain any spaces or commas. Suffixes of G (gigabytes) and M (megabytes) are allowed.</small></p>" +
|
||||||
|
"<p><small>For unlimited storage enter 0 (zero)</small></p>"),
|
||||||
|
"Set Quota",
|
||||||
|
function() {
|
||||||
|
api(
|
||||||
|
"/mail/users/quota",
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
email: email,
|
||||||
|
quota: $('#users_set_quota').val()
|
||||||
|
},
|
||||||
|
function(r) {
|
||||||
|
show_users();
|
||||||
|
},
|
||||||
|
function(r) {
|
||||||
|
show_modal_error("Set Quota", r);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function users_remove(elem) {
|
function users_remove(elem) {
|
||||||
var email = $(elem).parents('tr').attr('data-email');
|
var email = $(elem).parents('tr').attr('data-email');
|
||||||
|
|
||||||
|
@ -293,7 +385,7 @@ function generate_random_password() {
|
||||||
var charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; // confusable characters skipped
|
var charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; // confusable characters skipped
|
||||||
for (var i = 0; i < 12; i++)
|
for (var i = 0; i < 12; i++)
|
||||||
pw += charset.charAt(Math.floor(Math.random() * charset.length));
|
pw += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||||
show_modal_error("Random Password", "<p>Here, try this:</p> <p><code style='font-size: 110%'>" + pw + "</code></pr");
|
show_modal_error("Random Password", "<p>Here, try this:</p> <p><code style='font-size: 110%'>" + pw + "</code></p>");
|
||||||
return false; // cancel click
|
return false; // cancel click
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -51,9 +51,9 @@ if [[ $EUID -ne 0 ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clone the Mail-in-a-Box repository if it doesn't exist.
|
# 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
|
if [ ! -f /usr/bin/git ]; then
|
||||||
echo Installing git . . .
|
echo "Installing git . . ."
|
||||||
apt-get -q -q update
|
apt-get -q -q update
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null
|
DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null
|
||||||
echo
|
echo
|
||||||
|
@ -63,25 +63,25 @@ if [ ! -d $HOME/mailinabox ]; then
|
||||||
SOURCE=https://github.com/mail-in-a-box/mailinabox
|
SOURCE=https://github.com/mail-in-a-box/mailinabox
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo Downloading Mail-in-a-Box $TAG. . .
|
echo "Downloading Mail-in-a-Box $TAG. . ."
|
||||||
git clone \
|
git clone \
|
||||||
-b $TAG --depth 1 \
|
-b "$TAG" --depth 1 \
|
||||||
$SOURCE \
|
"$SOURCE" \
|
||||||
$HOME/mailinabox \
|
"$HOME/mailinabox" \
|
||||||
< /dev/null 2> /dev/null
|
< /dev/null 2> /dev/null
|
||||||
|
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Change directory to it.
|
# Change directory to it.
|
||||||
cd $HOME/mailinabox
|
cd "$HOME/mailinabox" || exit
|
||||||
|
|
||||||
# Update it.
|
# Update it.
|
||||||
if [ "$TAG" != $(git describe --always) ]; then
|
if [ "$TAG" != "$(git describe --always)" ]; then
|
||||||
echo Updating Mail-in-a-Box to $TAG . . .
|
echo "Updating Mail-in-a-Box to $TAG . . ."
|
||||||
git fetch --depth 1 --force --prune origin tag $TAG
|
git fetch --depth 1 --force --prune origin tag "$TAG"
|
||||||
if ! git checkout -q $TAG; then
|
if ! git checkout -q "$TAG"; then
|
||||||
echo "Update failed. Did you modify something in $(pwd)?"
|
echo "Update failed. Did you modify something in $PWD?"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
|
@ -89,4 +89,3 @@ fi
|
||||||
|
|
||||||
# Start setup script.
|
# Start setup script.
|
||||||
setup/start.sh
|
setup/start.sh
|
||||||
|
|
||||||
|
|
|
@ -45,8 +45,8 @@ apt_install \
|
||||||
# - https://www.dovecot.org/list/dovecot/2012-August/137569.html
|
# - https://www.dovecot.org/list/dovecot/2012-August/137569.html
|
||||||
# - https://www.dovecot.org/list/dovecot/2011-December/132455.html
|
# - https://www.dovecot.org/list/dovecot/2011-December/132455.html
|
||||||
tools/editconf.py /etc/dovecot/conf.d/10-master.conf \
|
tools/editconf.py /etc/dovecot/conf.d/10-master.conf \
|
||||||
default_process_limit=$(echo "$(nproc) * 250" | bc) \
|
default_process_limit="$(($(nproc) * 250))" \
|
||||||
default_vsz_limit=$(echo "$(free -tm | tail -1 | awk '{print $2}') / 3" | bc)M \
|
default_vsz_limit="$(($(free -tm | tail -1 | awk '{print $2}') / 3))M" \
|
||||||
log_path=/var/log/mail.log
|
log_path=/var/log/mail.log
|
||||||
|
|
||||||
# The inotify `max_user_instances` default is 128, which constrains
|
# The inotify `max_user_instances` default is 128, which constrains
|
||||||
|
@ -61,12 +61,38 @@ tools/editconf.py /etc/sysctl.conf \
|
||||||
# username part of the user's email address. We'll ensure that no bad domains or email addresses
|
# username part of the user's email address. We'll ensure that no bad domains or email addresses
|
||||||
# are created within the management daemon.
|
# are created within the management daemon.
|
||||||
tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
|
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 \
|
mail_privileged_group=mail \
|
||||||
first_valid_uid=0
|
first_valid_uid=0
|
||||||
|
|
||||||
# Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive.
|
# Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive.
|
||||||
cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf
|
cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/
|
||||||
|
sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf
|
||||||
|
if ! grep -q "mail_plugins = \$mail_plugins imap_quota" /etc/dovecot/conf.d/20-imap.conf; then
|
||||||
|
sed -i "s/mail_plugins = .*/mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# configure stuff for quota support
|
||||||
|
if ! grep -q "quota_status_success = DUNNO" /etc/dovecot/conf.d/90-quota.conf; then
|
||||||
|
cat > /etc/dovecot/conf.d/90-quota.conf << EOF;
|
||||||
|
plugin {
|
||||||
|
quota = maildir
|
||||||
|
|
||||||
|
quota_grace = 10%
|
||||||
|
|
||||||
|
quota_status_success = DUNNO
|
||||||
|
quota_status_nouser = DUNNO
|
||||||
|
quota_status_overquota = "522 5.2.2 Mailbox is full"
|
||||||
|
}
|
||||||
|
|
||||||
|
service quota-status {
|
||||||
|
executable = quota-status -p postfix
|
||||||
|
inet_listener {
|
||||||
|
port = 12340
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
# ### IMAP/POP
|
# ### IMAP/POP
|
||||||
|
|
||||||
|
@ -152,7 +178,7 @@ EOF
|
||||||
# Setting a `postmaster_address` is required or LMTP won't start. An alias
|
# Setting a `postmaster_address` is required or LMTP won't start. An alias
|
||||||
# will be created automatically by our management daemon.
|
# will be created automatically by our management daemon.
|
||||||
tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \
|
tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \
|
||||||
postmaster_address=postmaster@$PRIMARY_HOSTNAME
|
"postmaster_address=postmaster@$PRIMARY_HOSTNAME"
|
||||||
|
|
||||||
# ### Sieve
|
# ### Sieve
|
||||||
|
|
||||||
|
@ -201,14 +227,14 @@ chown -R mail:dovecot /etc/dovecot
|
||||||
chmod -R o-rwx /etc/dovecot
|
chmod -R o-rwx /etc/dovecot
|
||||||
|
|
||||||
# Ensure mailbox files have a directory that exists and are owned by the mail user.
|
# Ensure mailbox files have a directory that exists and are owned by the mail user.
|
||||||
mkdir -p $STORAGE_ROOT/mail/mailboxes
|
mkdir -p "$STORAGE_ROOT/mail/mailboxes"
|
||||||
chown -R mail:mail $STORAGE_ROOT/mail/mailboxes
|
chown -R mail:mail "$STORAGE_ROOT/mail/mailboxes"
|
||||||
|
|
||||||
# Same for the sieve scripts.
|
# Same for the sieve scripts.
|
||||||
mkdir -p $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_before"
|
||||||
mkdir -p $STORAGE_ROOT/mail/sieve/global_after
|
mkdir -p "$STORAGE_ROOT/mail/sieve/global_after"
|
||||||
chown -R mail:mail $STORAGE_ROOT/mail/sieve
|
chown -R mail:mail "$STORAGE_ROOT/mail/sieve"
|
||||||
|
|
||||||
# Allow the IMAP/POP ports in the firewall.
|
# Allow the IMAP/POP ports in the firewall.
|
||||||
ufw_allow imaps
|
ufw_allow imaps
|
||||||
|
|
|
@ -55,9 +55,9 @@ apt_install postfix postfix-sqlite postfix-pcre postgrey ca-certificates
|
||||||
# * Set the SMTP banner (which must have the hostname first, then anything).
|
# * Set the SMTP banner (which must have the hostname first, then anything).
|
||||||
tools/editconf.py /etc/postfix/main.cf \
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
inet_interfaces=all \
|
inet_interfaces=all \
|
||||||
smtp_bind_address=$PRIVATE_IP \
|
smtp_bind_address="$PRIVATE_IP" \
|
||||||
smtp_bind_address6=$PRIVATE_IPV6 \
|
smtp_bind_address6="$PRIVATE_IPV6" \
|
||||||
myhostname=$PRIMARY_HOSTNAME\
|
myhostname="$PRIMARY_HOSTNAME"\
|
||||||
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
|
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
|
||||||
mydestination=localhost
|
mydestination=localhost
|
||||||
|
|
||||||
|
@ -138,9 +138,9 @@ sed -i "s/PUBLIC_IP/$PUBLIC_IP/" /etc/postfix/outgoing_mail_header_filters
|
||||||
tools/editconf.py /etc/postfix/main.cf \
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
smtpd_tls_security_level=may\
|
smtpd_tls_security_level=may\
|
||||||
smtpd_tls_auth_only=yes \
|
smtpd_tls_auth_only=yes \
|
||||||
smtpd_tls_cert_file=$STORAGE_ROOT/ssl/ssl_certificate.pem \
|
smtpd_tls_cert_file="$STORAGE_ROOT/ssl/ssl_certificate.pem" \
|
||||||
smtpd_tls_key_file=$STORAGE_ROOT/ssl/ssl_private_key.pem \
|
smtpd_tls_key_file="$STORAGE_ROOT/ssl/ssl_private_key.pem" \
|
||||||
smtpd_tls_dh1024_param_file=$STORAGE_ROOT/ssl/dh2048.pem \
|
smtpd_tls_dh1024_param_file="$STORAGE_ROOT/ssl/dh2048.pem" \
|
||||||
smtpd_tls_protocols="!SSLv2,!SSLv3" \
|
smtpd_tls_protocols="!SSLv2,!SSLv3" \
|
||||||
smtpd_tls_ciphers=medium \
|
smtpd_tls_ciphers=medium \
|
||||||
tls_medium_cipherlist=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA \
|
tls_medium_cipherlist=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA \
|
||||||
|
@ -238,7 +238,7 @@ tools/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit=
|
||||||
# "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC
|
# "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 \
|
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=127.0.1.[2..99]" \
|
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org=127.0.1.[2..99]" \
|
||||||
smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org=127.0.0.[2..11],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=127.0.0.[2..11],reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023,check_policy_service inet:127.0.0.1:12340"
|
||||||
|
|
||||||
# Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that
|
# 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).
|
# Postgrey listens on the same interface (and not IPv6, for instance).
|
||||||
|
@ -260,17 +260,17 @@ tools/editconf.py /etc/default/postgrey \
|
||||||
|
|
||||||
|
|
||||||
# If the $STORAGE_ROOT/mail/postgrey is empty, copy the postgrey database over from the old location
|
# If the $STORAGE_ROOT/mail/postgrey is empty, copy the postgrey database over from the old location
|
||||||
if [ ! -d $STORAGE_ROOT/mail/postgrey/db ]; then
|
if [ ! -d "$STORAGE_ROOT/mail/postgrey/db" ]; then
|
||||||
# Stop the service
|
# Stop the service
|
||||||
service postgrey stop
|
service postgrey stop
|
||||||
# Ensure the new paths for postgrey db exists
|
# Ensure the new paths for postgrey db exists
|
||||||
mkdir -p $STORAGE_ROOT/mail/postgrey/db
|
mkdir -p "$STORAGE_ROOT/mail/postgrey/db"
|
||||||
# Move over database files
|
# Move over database files
|
||||||
mv /var/lib/postgrey/* $STORAGE_ROOT/mail/postgrey/db/ || true
|
mv /var/lib/postgrey/* "$STORAGE_ROOT/mail/postgrey/db/" || true
|
||||||
fi
|
fi
|
||||||
# Ensure permissions are set
|
# Ensure permissions are set
|
||||||
chown -R postgrey:postgrey $STORAGE_ROOT/mail/postgrey/
|
chown -R postgrey:postgrey "$STORAGE_ROOT/mail/postgrey/"
|
||||||
chmod 700 $STORAGE_ROOT/mail/postgrey/{,db}
|
chmod 700 "$STORAGE_ROOT/mail/postgrey/"{,db}
|
||||||
|
|
||||||
# We are going to setup a newer whitelist for postgrey, the version included in the distribution is old
|
# We are going to setup a newer whitelist for postgrey, the version included in the distribution is old
|
||||||
cat > /etc/cron.daily/mailinabox-postgrey-whitelist << EOF;
|
cat > /etc/cron.daily/mailinabox-postgrey-whitelist << EOF;
|
||||||
|
|
|
@ -18,12 +18,14 @@ source /etc/mailinabox.conf # load global vars
|
||||||
db_path=$STORAGE_ROOT/mail/users.sqlite
|
db_path=$STORAGE_ROOT/mail/users.sqlite
|
||||||
|
|
||||||
# Create an empty database if it doesn't yet exist.
|
# Create an empty database if it doesn't yet exist.
|
||||||
if [ ! -f $db_path ]; then
|
if [ ! -f "$db_path" ]; then
|
||||||
echo Creating new user database: $db_path;
|
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 users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '', quota TEXT NOT NULL DEFAULT '0');" | 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;
|
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path";
|
||||||
echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 $db_path;
|
echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 "$db_path";
|
||||||
echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path;
|
echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path";
|
||||||
|
elif sqlite3 $db_path ".schema users" | grep --invert-match quota; then
|
||||||
|
echo "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';" | sqlite3 $db_path;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ### User Authentication
|
# ### User Authentication
|
||||||
|
@ -51,7 +53,7 @@ driver = sqlite
|
||||||
connect = $db_path
|
connect = $db_path
|
||||||
default_pass_scheme = SHA512-CRYPT
|
default_pass_scheme = SHA512-CRYPT
|
||||||
password_query = SELECT email as user, password FROM users WHERE email='%u';
|
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 WHERE email='%u';
|
user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home, '*:bytes=' || quota AS quota_rule FROM users WHERE email='%u';
|
||||||
iterate_query = SELECT email AS user FROM users;
|
iterate_query = SELECT email AS user FROM users;
|
||||||
EOF
|
EOF
|
||||||
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions
|
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions
|
||||||
|
@ -159,4 +161,5 @@ EOF
|
||||||
restart_service postfix
|
restart_service postfix
|
||||||
restart_service dovecot
|
restart_service dovecot
|
||||||
|
|
||||||
|
# force a recalculation of all user quotas
|
||||||
|
doveadm quota recalc -A
|
||||||
|
|
|
@ -22,8 +22,8 @@ source /etc/mailinabox.conf # load global vars
|
||||||
echo "Installing Roundcube (webmail)..."
|
echo "Installing Roundcube (webmail)..."
|
||||||
apt_install \
|
apt_install \
|
||||||
dbconfig-common \
|
dbconfig-common \
|
||||||
php${PHP_VER}-cli php${PHP_VER}-sqlite3 php${PHP_VER}-intl php${PHP_VER}-common php${PHP_VER}-curl php${PHP_VER}-imap \
|
php"${PHP_VER}"-cli php"${PHP_VER}"-sqlite3 php"${PHP_VER}"-intl php"${PHP_VER}"-common php"${PHP_VER}"-curl php"${PHP_VER}"-imap \
|
||||||
php${PHP_VER}-gd php${PHP_VER}-pspell php${PHP_VER}-mbstring libjs-jquery libjs-jquery-mousewheel libmagic1 \
|
php"${PHP_VER}"-gd php"${PHP_VER}"-pspell php"${PHP_VER}"-mbstring libjs-jquery libjs-jquery-mousewheel libmagic1 \
|
||||||
sqlite3
|
sqlite3
|
||||||
|
|
||||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
# Install Roundcube from source if it is not already present or if it is out of date.
|
||||||
|
@ -145,6 +145,7 @@ cat > $RCM_CONFIG <<EOF;
|
||||||
\$config['session_path'] = '/mail/';
|
\$config['session_path'] = '/mail/';
|
||||||
/* prevent CSRF, requires php 7.3+ */
|
/* prevent CSRF, requires php 7.3+ */
|
||||||
\$config['session_samesite'] = 'Strict';
|
\$config['session_samesite'] = 'Strict';
|
||||||
|
\$config['quota_zero_as_unlimited'] = true;
|
||||||
?>
|
?>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
@ -170,8 +171,8 @@ cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Create writable directories.
|
# Create writable directories.
|
||||||
mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
|
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
|
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.log
|
sudo -u www-data touch /var/log/roundcubemail/errors.log
|
||||||
|
@ -194,10 +195,10 @@ usermod -a -G dovecot www-data
|
||||||
|
|
||||||
# set permissions so that PHP can use users.sqlite
|
# set permissions so that PHP can use users.sqlite
|
||||||
# could use dovecot instead of www-data, but not sure it matters
|
# could use dovecot instead of www-data, but not sure it matters
|
||||||
chown root:www-data $STORAGE_ROOT/mail
|
chown root:www-data "$STORAGE_ROOT/mail"
|
||||||
chmod 775 $STORAGE_ROOT/mail
|
chmod 775 "$STORAGE_ROOT/mail"
|
||||||
chown root:www-data $STORAGE_ROOT/mail/users.sqlite
|
chown root:www-data "$STORAGE_ROOT/mail/users.sqlite"
|
||||||
chmod 664 $STORAGE_ROOT/mail/users.sqlite
|
chmod 664 "$STORAGE_ROOT/mail/users.sqlite"
|
||||||
|
|
||||||
# Fix Carddav permissions:
|
# Fix Carddav permissions:
|
||||||
chown -f -R root:www-data ${RCM_PLUGIN_DIR}/carddav
|
chown -f -R root:www-data ${RCM_PLUGIN_DIR}/carddav
|
||||||
|
@ -205,9 +206,9 @@ chown -f -R root:www-data ${RCM_PLUGIN_DIR}/carddav
|
||||||
chmod -R 774 ${RCM_PLUGIN_DIR}/carddav
|
chmod -R 774 ${RCM_PLUGIN_DIR}/carddav
|
||||||
|
|
||||||
# Run Roundcube database migration script (database is created if it does not exist)
|
# Run Roundcube database migration script (database is created if it does not exist)
|
||||||
php$PHP_VER ${RCM_DIR}/bin/updatedb.sh --dir ${RCM_DIR}/SQL --package roundcube
|
php"$PHP_VER" ${RCM_DIR}/bin/updatedb.sh --dir ${RCM_DIR}/SQL --package roundcube
|
||||||
chown www-data:www-data $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
|
chmod 664 "$STORAGE_ROOT/mail/roundcube/roundcube.sqlite"
|
||||||
|
|
||||||
# Patch the Roundcube code to eliminate an issue that causes postfix to reject our sqlite
|
# Patch the Roundcube code to eliminate an issue that causes postfix to reject our sqlite
|
||||||
# user database (see https://github.com/mail-in-a-box/mailinabox/issues/2185)
|
# user database (see https://github.com/mail-in-a-box/mailinabox/issues/2185)
|
||||||
|
@ -217,8 +218,8 @@ sed -i.miabold 's/^[^#]\+.\+PRAGMA journal_mode = WAL.\+$/#&/' \
|
||||||
# Because Roundcube wants to set the PRAGMA we just deleted from the source, we apply it here
|
# Because Roundcube wants to set the PRAGMA we just deleted from the source, we apply it here
|
||||||
# to the roundcube database (see https://github.com/roundcube/roundcubemail/issues/8035)
|
# to the roundcube database (see https://github.com/roundcube/roundcubemail/issues/8035)
|
||||||
# Database should exist, created by migration script
|
# Database should exist, created by migration script
|
||||||
hide_output sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite 'PRAGMA journal_mode=WAL;'
|
hide_output sqlite3 "$STORAGE_ROOT/mail/roundcube/roundcube.sqlite" 'PRAGMA journal_mode=WAL;'
|
||||||
|
|
||||||
# Enable PHP modules.
|
# Enable PHP modules.
|
||||||
phpenmod -v $PHP_VER imap
|
phpenmod -v "$PHP_VER" imap
|
||||||
restart_service php$PHP_VER-fpm
|
restart_service php"$PHP_VER"-fpm
|
||||||
|
|
Loading…
Reference in New Issue