1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-18 18:07:22 +01:00

Compare commits

..

8 Commits

Author SHA1 Message Date
Joshua Tauberer
1b3e5e818c v69 2024-07-20 07:33:07 -04:00
Joshua Tauberer
2f5e736fa0 Clean up Nextcloud email settings for calendar invitations
I don't think anything is actually changed here but I think my box was missing some of these settings.
2024-07-20 07:27:46 -04:00
Michael Heuberger
f118a6c0bf Apply small Nextcloud upgrade to 26.0.13 (#2401) 2024-07-08 08:21:12 -04:00
jvolkenant
de0fc796d4 Fix chown during Nexcloud upgrades (#2377) 2024-06-18 08:37:01 -04:00
Matt
4dd1e75ee7 Allow for Union[None, List[datetime.datetime]] values when printing user table in weekly mail logs (#2378)
* Fix - Allow for `Union[None, List[datetime.datetime]]` when printing user
tables for the weekly mail logs.

* Add - ruff suppressions.
2024-06-18 08:35:54 -04:00
John James Jacoby
8b9f0489c8 Add custom.yaml support for WebSockets (#2385)
Fixes #1956.
2024-06-18 08:32:11 -04:00
matidau
6321ce6ef0 Add php8.0-intl package to z-push setup (#2389) 2024-06-18 08:29:51 -04:00
matidau
30d78cd35a Update zpush.sh to version 2.7.3 (#2390) 2024-06-18 08:29:22 -04:00
17 changed files with 68 additions and 414 deletions

View File

@@ -1,6 +1,21 @@
CHANGELOG CHANGELOG
========= =========
Version 69 (July 20, 2024)
--------------------------
Package updates:
* Nextcloud is updated to 26.0.13.
* Z-Push is updated to 2.7.3.
Other updates:
* Fixed an error generating the weekly statistics.
* Fixed file permissions when setting up Nextcloud.
* Added an undocumented option to proxy websockets.
* Internal improvements to the code to make it more reliable and readable.
Version 68 (April 1, 2024) Version 68 (April 1, 2024)
-------------------------- --------------------------
@@ -25,7 +40,6 @@ Other:
* fail2ban is updated to see "HTTP/2.0" requests to munin also. * fail2ban is updated to see "HTTP/2.0" requests to munin also.
* Internal improvements to the code to make it more reliable and readable. * Internal improvements to the code to make it more reliable and readable.
Version 67 (December 22, 2023) Version 67 (December 22, 2023)
------------------------------ ------------------------------

View File

@@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele
$ git clone https://github.com/mail-in-a-box/mailinabox $ git clone https://github.com/mail-in-a-box/mailinabox
$ cd mailinabox $ cd mailinabox
$ git checkout v68 $ git checkout v69
Begin the installation. Begin the installation.

View File

@@ -1,63 +0,0 @@
## 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
}
}

View File

@@ -60,13 +60,11 @@ 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)
@@ -90,10 +88,6 @@ 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"}:
@@ -123,14 +117,6 @@ 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)
@@ -152,12 +138,6 @@ 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)

View File

@@ -21,7 +21,6 @@ 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
@@ -192,31 +191,8 @@ 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', ''), quota, env) return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), 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)
@@ -675,29 +651,6 @@ 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/')

View File

@@ -679,7 +679,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None
data_accum[col] += d[row] data_accum[col] += d[row]
try: try:
if None not in {latest, earliest}: if None not in [latest, earliest]: # noqa PLR6201
vert_pos = len(line) vert_pos = len(line)
e = earliest[row] e = earliest[row]
l = latest[row] l = latest[row]
@@ -732,7 +732,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None
else: else:
header += l.rjust(max(5, len(l) + 1, col_widths[col])) header += l.rjust(max(5, len(l) + 1, col_widths[col]))
if None not in {latest, earliest}: if None not in [latest, earliest]: # noqa PLR6201
header += " │ timespan " header += " │ timespan "
lines.insert(0, header.rstrip()) lines.insert(0, header.rstrip())
@@ -757,7 +757,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None
footer += temp.format(data_accum[row]) footer += temp.format(data_accum[row])
try: try:
if None not in {latest, earliest}: if None not in [latest, earliest]: # noqa PLR6201
max_l = max(latest) max_l = max(latest)
min_e = min(earliest) min_e = min(earliest)
timespan = relativedelta(max_l, min_e) timespan = relativedelta(max_l, min_e)

View File

@@ -10,8 +10,6 @@
# address entered by the user. # address entered by the user.
import os, sqlite3, re import os, sqlite3, re
import subprocess
import utils import utils
from email_validator import validate_email as validate_email_, EmailNotValidError from email_validator import validate_email as validate_email_, EmailNotValidError
import idna import idna
@@ -104,18 +102,6 @@ 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.
@@ -139,46 +125,13 @@ 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, quota FROM users') c.execute('SELECT email, privileges FROM users')
for email, privileges, quota in c.fetchall(): for email, privileges 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)
@@ -197,10 +150,6 @@ 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)
@@ -317,7 +266,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, quota, env): def add_mail_user(email, pw, privs, env):
# validate email # validate email
if email.strip() == "": if email.strip() == "":
return ("No email address provided.", 400) return ("No email address provided.", 400)
@@ -343,14 +292,6 @@ def add_mail_user(email, pw, privs, quota, 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)
@@ -359,16 +300,14 @@ def add_mail_user(email, pw, privs, quota, env):
# add the user to the database # add the user to the database
try: try:
c.execute("INSERT INTO users (email, password, privileges, quota) VALUES (?, ?, ?, ?)", c.execute("INSERT INTO users (email, password, privileges) VALUES (?, ?, ?)",
(email, pw, "\n".join(privs), quota)) (email, pw, "\n".join(privs)))
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")
@@ -393,59 +332,6 @@ 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.

View File

@@ -6,7 +6,6 @@
#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>
@@ -28,10 +27,6 @@
<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%;">
@@ -39,18 +34,13 @@
<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="35%">Email Address</th> <th width="50%">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>
@@ -63,22 +53,10 @@
<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
@@ -119,28 +97,10 @@
<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> <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>
<td>POST</td> <tr><td>POST</td><td>/remove</td> <td>Removes a mail user. Required POST-body parameter is <code>email</code>.</td></tr>
<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>
@@ -165,15 +125,6 @@ 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",
@@ -182,7 +133,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='6' style='background-color: #EEE'></th></tr>"); var hdr = $("<tr><th colspan='2' 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);
@@ -200,18 +151,7 @@ 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.attr('data-quota', user.quota); n.find('.address').text(user.email)
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;
@@ -240,15 +180,13 @@ 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.
@@ -290,36 +228,6 @@ 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');
@@ -385,7 +293,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></p>"); show_modal_error("Random Password", "<p>Here, try this:</p> <p><code style='font-size: 110%'>" + pw + "</code></pr");
return false; // cancel click return false; // cancel click
} }
</script> </script>

View File

@@ -166,6 +166,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
pass_http_host_header = False pass_http_host_header = False
proxy_redirect_off = False proxy_redirect_off = False
frame_options_header_sameorigin = False frame_options_header_sameorigin = False
web_sockets = False
m = re.search("#(.*)$", url) m = re.search("#(.*)$", url)
if m: if m:
for flag in m.group(1).split(","): for flag in m.group(1).split(","):
@@ -175,6 +176,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
proxy_redirect_off = True proxy_redirect_off = True
elif flag == "frame-options-sameorigin": elif flag == "frame-options-sameorigin":
frame_options_header_sameorigin = True frame_options_header_sameorigin = True
elif flag == "web-sockets":
web_sockets = True
url = re.sub("#(.*)$", "", url) url = re.sub("#(.*)$", "", url)
nginx_conf_extra += "\tlocation %s {" % path nginx_conf_extra += "\tlocation %s {" % path
@@ -185,6 +188,10 @@ def make_domain_config(domain, templates, ssl_certificates, env):
nginx_conf_extra += "\n\t\tproxy_set_header Host $http_host;" nginx_conf_extra += "\n\t\tproxy_set_header Host $http_host;"
if frame_options_header_sameorigin: if frame_options_header_sameorigin:
nginx_conf_extra += "\n\t\tproxy_set_header X-Frame-Options SAMEORIGIN;" nginx_conf_extra += "\n\t\tproxy_set_header X-Frame-Options SAMEORIGIN;"
if web_sockets:
nginx_conf_extra += "\n\t\tproxy_http_version 1.1;"
nginx_conf_extra += "\n\t\tproxy_set_header Upgrade $http_upgrade;"
nginx_conf_extra += "\n\t\tproxy_set_header Connection 'Upgrade';"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;" nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Host $http_host;" nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Host $http_host;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Proto $scheme;" nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Proto $scheme;"

View File

@@ -23,7 +23,7 @@ if [ -z "$TAG" ]; then
if [ "$UBUNTU_VERSION" == "Ubuntu 22.04 LTS" ]; then if [ "$UBUNTU_VERSION" == "Ubuntu 22.04 LTS" ]; then
# This machine is running Ubuntu 22.04, which is supported by # This machine is running Ubuntu 22.04, which is supported by
# Mail-in-a-Box versions 60 and later. # Mail-in-a-Box versions 60 and later.
TAG=v68 TAG=v69
elif [ "$UBUNTU_VERSION" == "Ubuntu 18.04 LTS" ]; then elif [ "$UBUNTU_VERSION" == "Ubuntu 18.04 LTS" ]; then
# This machine is running Ubuntu 18.04, which is supported by # This machine is running Ubuntu 18.04, which is supported by
# Mail-in-a-Box versions 0.40 through 5x. # Mail-in-a-Box versions 0.40 through 5x.
@@ -89,3 +89,4 @@ fi
# Start setup script. # Start setup script.
setup/start.sh setup/start.sh

View File

@@ -66,33 +66,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
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/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf
sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf
if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then
sed -i "s/\(mail_plugins =.*\)/\1\n 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

View File

@@ -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,check_policy_service inet:127.0.0.1:12340" 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"
# 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).

View File

@@ -20,12 +20,10 @@ 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 '', quota TEXT NOT NULL DEFAULT '0');" | 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 '');" | 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
@@ -53,7 +51,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, '*:bytes=' || quota AS quota_rule 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';
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
@@ -161,5 +159,4 @@ EOF
restart_service postfix restart_service postfix
restart_service dovecot restart_service dovecot
# force a recalculation of all user quotas
doveadm quota recalc -A

View File

@@ -21,8 +21,8 @@ echo "Installing Nextcloud (contacts/calendar)..."
# we automatically install intermediate versions as needed. # we automatically install intermediate versions as needed.
# * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and # * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and
# copying it from the error message when it doesn't match what is below. # copying it from the error message when it doesn't match what is below.
nextcloud_ver=26.0.12 nextcloud_ver=26.0.13
nextcloud_hash=b55e9f51171c0a9b9ab3686cf5c8ad1a4292ca15 nextcloud_hash=d5c10b650e5396d5045131c6d22c02a90572527c
# Nextcloud apps # Nextcloud apps
# -------------- # --------------
@@ -40,12 +40,12 @@ contacts_ver=5.5.3
contacts_hash=799550f38e46764d90fa32ca1a6535dccd8316e5 contacts_hash=799550f38e46764d90fa32ca1a6535dccd8316e5
# Always ensure the versions are supported, see https://apps.nextcloud.com/apps/calendar # Always ensure the versions are supported, see https://apps.nextcloud.com/apps/calendar
calendar_ver=4.6.6 calendar_ver=4.7.6
calendar_hash=e34a71669a52d997e319d64a984dcd041389eb22 calendar_hash=a995bca4effeecb2cab25f3bbeac9bfe05fee766
# Always ensure the versions are supported, see https://apps.nextcloud.com/apps/user_external # Always ensure the versions are supported, see https://apps.nextcloud.com/apps/user_external
user_external_ver=3.2.0 user_external_ver=3.3.0
user_external_hash=a494073dcdecbbbc79a9c77f72524ac9994d2eec user_external_hash=280d24eb2a6cb56b4590af8847f925c28d8d853e
# Developer advice (test plan) # Developer advice (test plan)
# ---------------------------- # ----------------------------
@@ -131,7 +131,7 @@ InstallNextcloud() {
# Make sure permissions are correct or the upgrade step won't run. # Make sure permissions are correct or the upgrade step won't run.
# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress # $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
# that error. # that error.
chown -f -R www-data:www-data "$STORAGE_ROOT/owncloud /usr/local/lib/owncloud" || /bin/true chown -f -R www-data:www-data "$STORAGE_ROOT/owncloud" /usr/local/lib/owncloud || /bin/true
# If this isn't a new installation, immediately run the upgrade script. # If this isn't a new installation, immediately run the upgrade script.
# Then check for success (0=ok and 3=no upgrade needed, both are success). # Then check for success (0=ok and 3=no upgrade needed, both are success).
@@ -274,15 +274,6 @@ if [ ! -f "$STORAGE_ROOT/owncloud/owncloud.db" ]; then
), ),
), ),
'memcache.local' => '\OC\Memcache\APCu', 'memcache.local' => '\OC\Memcache\APCu',
'mail_smtpmode' => 'sendmail',
'mail_smtpsecure' => '',
'mail_smtpauthtype' => 'LOGIN',
'mail_smtpauth' => false,
'mail_smtphost' => '',
'mail_smtpport' => '',
'mail_smtpname' => '',
'mail_smtppassword' => '',
'mail_from_address' => 'owncloud',
); );
?> ?>
EOF EOF
@@ -338,13 +329,10 @@ include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['memcache.local'] = '\OC\Memcache\APCu'; \$CONFIG['memcache.local'] = '\OC\Memcache\APCu';
\$CONFIG['overwrite.cli.url'] = 'https://${PRIMARY_HOSTNAME}/cloud'; \$CONFIG['overwrite.cli.url'] = 'https://${PRIMARY_HOSTNAME}/cloud';
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches our master administrator address
\$CONFIG['logtimezone'] = '$TIMEZONE'; \$CONFIG['logtimezone'] = '$TIMEZONE';
\$CONFIG['logdateformat'] = 'Y-m-d H:i:s'; \$CONFIG['logdateformat'] = 'Y-m-d H:i:s';
\$CONFIG['mail_domain'] = '$PRIMARY_HOSTNAME';
\$CONFIG['user_backends'] = array( \$CONFIG['user_backends'] = array(
array( array(
'class' => '\OCA\UserExternal\IMAP', 'class' => '\OCA\UserExternal\IMAP',
@@ -354,6 +342,16 @@ include("$STORAGE_ROOT/owncloud/config.php");
), ),
); );
\$CONFIG['mail_domain'] = '$PRIMARY_HOSTNAME';
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches the required administrator alias on mail_domain/$PRIMARY_HOSTNAME
\$CONFIG['mail_smtpmode'] = 'sendmail';
\$CONFIG['mail_smtpauth'] = true; # if smtpmode is smtp
\$CONFIG['mail_smtphost'] = '127.0.0.1'; # if smtpmode is smtp
\$CONFIG['mail_smtpport'] = '587'; # if smtpmode is smtp
\$CONFIG['mail_smtpsecure'] = ''; # if smtpmode is smtp, must be empty string
\$CONFIG['mail_smtpname'] = ''; # if smtpmode is smtp, set this to a mail user
\$CONFIG['mail_smtppassword'] = ''; # if smtpmode is smtp, set this to the user's password
echo "<?php\n\\\$CONFIG = "; echo "<?php\n\\\$CONFIG = ";
var_export(\$CONFIG); var_export(\$CONFIG);
echo ";"; echo ";";

View File

@@ -145,7 +145,6 @@ 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

View File

@@ -17,13 +17,13 @@ source /etc/mailinabox.conf # load global vars
echo "Installing Z-Push (Exchange/ActiveSync server)..." echo "Installing Z-Push (Exchange/ActiveSync server)..."
apt_install \ apt_install \
php"${PHP_VER}"-soap php"${PHP_VER}"-imap libawl-php php"$PHP_VER"-xml php"${PHP_VER}"-soap php"${PHP_VER}"-imap libawl-php php"$PHP_VER"-xml php"${PHP_VER}"-intl
phpenmod -v "$PHP_VER" imap phpenmod -v "$PHP_VER" imap
# Copy Z-Push into place. # Copy Z-Push into place.
VERSION=2.7.1 VERSION=2.7.3
TARGETHASH=f15c566b1ad50de24f3f08f505f0c3d8155c2d0d TARGETHASH=9d4bec41935e9a4e07880c5ff915bcddbda4443b
needs_update=0 #NODOC needs_update=0 #NODOC
if [ ! -f /usr/local/lib/z-push/version ]; then if [ ! -f /usr/local/lib/z-push/version ]; then
needs_update=1 #NODOC needs_update=1 #NODOC