1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2024-12-25 07:47:05 +00:00

Merge branch 'master' into extended

This commit is contained in:
John Supplee 2019-03-09 10:51:47 +02:00
commit 2c50c1876e
14 changed files with 601 additions and 23 deletions

132
README.md
View File

@ -1,3 +1,135 @@
Mail-in-a-Box with Quotas
=========================
This is an experimental implementation of Mail-in-a-box with quota support.
Quotas can be set and viewed in the control panel
To set quotas from the command line, use:
tools/mail.py user quota <email> <quota>
To set the system default quota for new users, use:
tools/mail.py system default-quota <quota>
Mailbox size recalculation by Dovecot can be forced using the command:
doveadm quota recalc -A
Please report any bugs on github.
Installing v0.41-quota
----------------------
To install the latest version, log into your box and execute the following commands:
$ git clone https://github.com/jrsupplee/mailinabox.git
$ cd mailinabox
$ sudo bash setup/bootstrap.sh
Follow the standard directions for setting up an MiaB installation. There are no special installation steps for installing this version.
The default quota is set to `0` which means unlimited. If you want to set a different default quota, follow the directions above.
Upgrading v0.41 to v.0.41-quota
-------------------------------
This is experimental software. You have been warned.
* Rename your `mailinabox` directory to something like `miab.old`
* Clone this repository using:
`git clone https://github.com/jrsupplee/mailinabox.git`
* cd into `mailinabox` and run `sudo setup/bootstrap.sh` On occasion there are lock errors when updating `Munin`. Just re-run `sudo setup/start.sh` until the error does not occur.
* Note: all existing users at the time of the upgrade will have there quota set to `0` (unlimited).
Upgrading MiaB with quotas to a New Version
---------------------------------------
* `cd` into the `mailinabox` directory.
* Execute `git pull` to download the latest changes.
* Execute `sudo bash setup/bootstrap.sh` to checkout the latest version and re-run setup.
Issues
------
* When a user's quota is changed, any IMAP session running for that user will not recognize the new quota. To solve this a `dovecot reload` could be issued causing all current IMAP sessions to be terminated. On a system with many users, it might not be desirable to reset all users sessions to fix the quota for one user. Also if the administrator is setting the quota for several users it would result in the continual reset of those connections.
Changes
-------
### v0.41-quota-0.17-alpha
* Update the README
### v0.41-quota-0.16-alpha
* Update to v0.41 of Mail-in-a-Box
### v0.40-quota-0.16-alpha
* Fix problem with quota field on control panel that prevented adding users.
### v0.40-quota-0.15-alpha
* Fix bug where quotas are not recalculated when a user's quota is changed in control panel
### v0.40-quota-0.14-alpha
* When updating a user's quota, execute `doveadm quota recalc -u <email>` to forces an immediate recalculation of the user's quota.
* Add a thousands separator (,) to the messages count in the control panel user list.
* Execute `doveadm quota recalc -A` to force a recalculation of all user quotas when running `start.sh`.
* Get rid of the error message complaining that the `quota` column already exists when upgrading from a previous version of `v0.40-quota`.
### v0.40-quota-0.13-alpha
* Add a `default-quota` setting in `settings.yaml`.
* Add input for setting quota when entering a new user in control panel.
* Modify `tools/mail.py` to allow for setting and getting the default system quota.
* Modify `tools/mail.py` to allow for getting a user's quota setting.
* Modify the mail users list in control panel to display percentage of quota used.
### v0.40-quota-0.12-alpha
* Update README
### v0.40-quota-0.11-alpha
* Read latest version from this repository not the Mail-in-a-Box master repository
### v0.40-quota-0.1-alpha
* First experimental release of Mail-in-a-Box for quotas.
* Quotas are working and there is basic support in the control panel and `tools/mail.py`.
Reference Documents
-------------------
* https://blog.sys4.de/postfix-dovecot-mailbox-quota-en.html
* https://linuxize.com/post/install-and-configure-postfix-and-dovecot/
\[BEGIN Official README]
Mail-in-a-Box Mail-in-a-Box
============= =============

View File

@ -0,0 +1,94 @@
##
## IMAP specific settings
##
# If nothing happens for this long while client is IDLEing, move the connection
# to imap-hibernate process and close the old imap process. This saves memory,
# because connections use very little memory in imap-hibernate process. The
# downside is that recreating the imap process back uses some resources.
#imap_hibernate_timeout = 0
# Maximum IMAP command line length. Some clients generate very long command
# lines with huge mailboxes, so you may need to raise this if you get
# "Too long argument" or "IMAP command line too large" errors often.
#imap_max_line_length = 64k
# IMAP logout format string:
# %i - total number of bytes read from client
# %o - total number of bytes sent to client
# %{fetch_hdr_count} - Number of mails with mail header data sent to client
# %{fetch_hdr_bytes} - Number of bytes with mail header data sent to client
# %{fetch_body_count} - Number of mails with mail body data sent to client
# %{fetch_body_bytes} - Number of bytes with mail body data sent to client
# %{deleted} - Number of mails where client added \Deleted flag
# %{expunged} - Number of mails that client expunged, which does not
# include automatically expunged mails
# %{autoexpunged} - Number of mails that were automatically expunged after
# client disconnected
# %{trashed} - Number of mails that client copied/moved to the
# special_use=\Trash mailbox.
# %{appended} - Number of mails saved during the session
#imap_logout_format = in=%i out=%o
# Override the IMAP CAPABILITY response. If the value begins with '+',
# add the given capabilities on top of the defaults (e.g. +XFOO XBAR).
#imap_capability =
# How long to wait between "OK Still here" notifications when client is
# IDLEing.
#imap_idle_notify_interval = 2 mins
imap_idle_notify_interval=4 mins
# ID field names and values to send to clients. Using * as the value makes
# Dovecot use the default value. The following fields have default values
# currently: name, version, os, os-version, support-url, support-email.
#imap_id_send =
# ID fields sent by client to log. * means everything.
#imap_id_log =
# Workarounds for various client bugs:
# delay-newmail:
# Send EXISTS/RECENT new mail notifications only when replying to NOOP
# and CHECK commands. Some clients ignore them otherwise, for example OSX
# Mail (<v2.1). Outlook Express breaks more badly though, without this it
# may show user "Message no longer in server" errors. Note that OE6 still
# breaks even with this workaround if synchronization is set to
# "Headers Only".
# tb-extra-mailbox-sep:
# Thunderbird gets somehow confused with LAYOUT=fs (mbox and dbox) and
# adds extra '/' suffixes to mailbox names. This option causes Dovecot to
# ignore the extra '/' instead of treating it as invalid mailbox name.
# tb-lsub-flags:
# Show \Noselect flags for LSUB replies with LAYOUT=fs (e.g. mbox).
# This makes Thunderbird realize they aren't selectable and show them
# greyed out, instead of only later giving "not selectable" popup error.
#
# The list is space-separated.
#imap_client_workarounds =
# Host allowed in URLAUTH URLs sent by client. "*" allows all.
#imap_urlauth_host =
# What happens when FETCH fails due to some internal error:
# disconnect-immediately:
# The FETCH is aborted immediately and the IMAP client is disconnected.
# disconnect-after:
# The FETCH runs for all the requested mails returning as much data as
# possible. The client is finally disconnected without a tagged reply.
# no-after:
# Same as disconnect-after, but tagged NO reply is sent instead of
# disconnecting the client. If the client attempts to FETCH the same failed
# mail more than once, the client is disconnected. This is to avoid clients
# from going into infinite loops trying to FETCH a broken mail.
#imap_fetch_failure = disconnect-immediately
mail_plugins = $mail_plugins quota
protocol imap {
# Space separated list of plugins to load (default is global mail_plugins).
mail_plugins = $mail_plugins antispam imap_quota
# Maximum number of IMAP connections allowed for a user from each IP address.
# NOTE: The username is compared case-sensitively.
#mail_max_userip_connections = 10
}

View File

@ -0,0 +1,98 @@
##
## Quota configuration.
##
# Note that you also have to enable quota plugin in mail_plugins setting.
# <doc/wiki/Quota.txt>
##
## Quota limits
##
# Quota limits are set using "quota_rule" parameters. To get per-user quota
# limits, you can set/override them by returning "quota_rule" extra field
# from userdb. It's also possible to give mailbox-specific limits, for example
# to give additional 100 MB when saving to Trash:
plugin {
#quota_rule = *:storage=1G
#quota_rule2 = Trash:storage=+100M
# LDA/LMTP allows saving the last mail to bring user from under quota to
# over quota, if the quota doesn't grow too high. Default is to allow as
# long as quota will stay under 10% above the limit. Also allowed e.g. 10M.
#quota_grace = 10%%
# Quota plugin can also limit the maximum accepted mail size.
#quota_max_mail_size = 100M
}
##
## Quota warnings
##
# You can execute a given command when user exceeds a specified quota limit.
# Each quota root has separate limits. Only the command for the first
# exceeded limit is excecuted, so put the highest limit first.
# The commands are executed via script service by connecting to the named
# UNIX socket (quota-warning below).
# Note that % needs to be escaped as %%, otherwise "% " expands to empty.
plugin {
#quota_warning = storage=95%% quota-warning 95 %u
#quota_warning2 = storage=80%% quota-warning 80 %u
}
# Example quota-warning service. The unix listener's permissions should be
# set in a way that mail processes can connect to it. Below example assumes
# that mail processes run as vmail user. If you use mode=0666, all system users
# can generate quota warnings to anyone.
#service quota-warning {
# executable = script /usr/local/bin/quota-warning.sh
# user = dovecot
# unix_listener quota-warning {
# user = vmail
# }
#}
##
## Quota backends
##
# Multiple backends are supported:
# dirsize: Find and sum all the files found from mail directory.
# Extremely SLOW with Maildir. It'll eat your CPU and disk I/O.
# dict: Keep quota stored in dictionary (eg. SQL)
# maildir: Maildir++ quota
# fs: Read-only support for filesystem quota
plugin {
quota = maildir
quota_grace = 10%
quota_status_success = DUNNO
quota_status_nouser = DUNNO
quota_status_overquota = "522 5.2.2 Mailbox is full"
#quota = dirsize:User quota
#quota = maildir:User quota
#quota = dict:User quota::proxy::quota
#quota = fs:User quota
}
service quota-status {
executable = quota-status -p postfix
inet_listener {
port = 12340
}
}
# Multiple quota roots are also possible, for example this gives each user
# their own 100MB quota and one shared 1GB quota within the domain:
plugin {
#quota = dict:user::proxy::quota
#quota2 = dict:domain:%d:proxy::quota_domain
#quota_rule = *:storage=102400
#quota2_rule = *:storage=1048576
}

View File

@ -11,7 +11,7 @@ import auth, utils, multiprocessing.pool
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user from mailconfig import get_mail_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
env = utils.load_environment() env = utils.load_environment()
auth_service = auth.KeyAuthService() auth_service = auth.KeyAuthService()
@ -155,8 +155,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)
@ -522,6 +545,33 @@ def privacy_status_set():
utils.write_settings(config, env) utils.write_settings(config, env)
return "OK" return "OK"
# Quotas
@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"
# Mailgraph # Mailgraph
@app.route('/mailgraph/image.cgi', methods=['GET']) @app.route('/mailgraph/image.cgi', methods=['GET'])

View File

@ -105,6 +105,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.
@ -128,13 +140,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)
@ -266,7 +311,7 @@ def get_mail_domains(env, filter_aliases=lambda alias : True):
+ [get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ] + [get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ]
) )
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 +337,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 +353,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 +387,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.

View File

@ -787,7 +787,7 @@ def get_latest_miab_version():
from socket import timeout from socket import timeout
try: try:
return re.search(b'TAG=(.*)', urlopen("https://mailinabox.email/setup.sh?ping=1", timeout=5).read()).group(1).decode("utf8") return re.search(b'TAG=(.*)', urlopen("https://raw.githubusercontent.com/jrsupplee/mailinabox/master/setup/bootstrap.sh", timeout=5).read()).group(1).decode("utf8")
except (HTTPError, URLError, timeout): except (HTTPError, URLError, timeout):
return None return None
@ -805,11 +805,11 @@ def check_miab_version(env, output):
latest_ver = get_latest_miab_version() latest_ver = get_latest_miab_version()
if this_ver == latest_ver: if this_ver == latest_ver:
output.print_ok("Mail-in-a-Box is up to date. You are running version %s." % this_ver) output.print_ok("Mail-in-a-Box with quota support is up to date. You are running version %s." % this_ver)
elif latest_ver is None: elif latest_ver is None:
output.print_error("Latest Mail-in-a-Box version could not be determined. You are running version %s." % this_ver) output.print_error("Latest Mail-in-a-Box version could not be determined. You are running version %s." % this_ver)
else: else:
output.print_error("A new version of Mail-in-a-Box is available. You are running version %s. The latest version is %s. For upgrade instructions, see https://mailinabox.email. " output.print_error("A new version of Mail-in-a-Box is available. You are running version %s. The latest version is %s. For upgrade instructions, see https://github.com/jrsupplee/mailinabox/blob/master/README.md. "
% (this_ver, latest_ver)) % (this_ver, latest_ver))
def run_and_output_changes(env, pool): def run_and_output_changes(env, pool):

View File

@ -7,6 +7,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>
@ -28,6 +29,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%;">
@ -35,13 +40,18 @@
<li>Use <a href="#" onclick="return show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.</li> <li>Use <a href="#" onclick="return show_panel('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="#" onclick="return show_panel('aliases');">aliases</a> can.</li> <li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#" onclick="return show_panel('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>
@ -54,10 +64,19 @@
<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>
<a href="#" onclick="users_set_quota(this); return false;" class='setquota' title="Set Quota">
set quota
</a>
|
<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
@ -126,6 +145,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",
@ -134,7 +162,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><td colspan='3'><h4/></td></tr>"); var hdr = $("<tr><td colspan='6'><h4/></td></tr>");
hdr.find('h4').text(r[i].domain); hdr.find('h4').text(r[i].domain);
$('#user_table tbody').append(hdr); $('#user_table tbody').append(hdr);
@ -152,7 +180,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;
@ -181,13 +220,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.
@ -229,6 +270,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');
@ -294,7 +365,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>

View File

@ -20,7 +20,7 @@ if [ -z "$TAG" ]; then
# want to display in status checks. # want to display in status checks.
if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' `" == "Ubuntu 18.04 LTS" ]; then if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' `" == "Ubuntu 18.04 LTS" ]; then
# This machine is running Ubuntu 18.04. # This machine is running Ubuntu 18.04.
TAG=v0.41 TAG=v0.41-quota-0.17-alpha
elif [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' `" == "Ubuntu 14.04 LTS" ]; then elif [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' `" == "Ubuntu 14.04 LTS" ]; then
# This machine is running Ubuntu 14.04. # This machine is running Ubuntu 14.04.

View File

@ -66,7 +66,9 @@ 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-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/
cp conf/dovecot/conf.d/20-imap.conf /etc/dovecot/conf.d/
cp conf/dovecot/conf.d/90-quota.conf /etc/dovecot/conf.d/
# ### IMAP/POP # ### IMAP/POP

View File

@ -198,7 +198,7 @@ tools/editconf.py /etc/postfix/main.cf lmtp_destination_recipient_limit=1
# "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" \ smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org" \
smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023" smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023","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).

View File

@ -20,8 +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 '');" | 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;
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
@ -49,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 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
@ -153,4 +155,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

View File

@ -127,7 +127,7 @@ cat > $RCM_CONFIG <<EOF;
\$config['login_autocomplete'] = 2; \$config['login_autocomplete'] = 2;
\$config['password_charset'] = 'UTF-8'; \$config['password_charset'] = 'UTF-8';
\$config['junk_mbox'] = 'Spam'; \$config['junk_mbox'] = 'Spam';
?> \$config['quota_zero_as_unlimited'] = true;
EOF EOF
# Configure CardDav # Configure CardDav

View File

@ -57,9 +57,11 @@ def setup_key_auth(mgmt_uri):
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Usage: ") print("Usage: ")
print(" tools/mail.py user (lists users)") print(" tools/mail.py system default-quota [new default]")
print(" tools/mail.py user (lists users with quotas)")
print(" tools/mail.py user add user@domain.com [password]") print(" tools/mail.py user add user@domain.com [password]")
print(" tools/mail.py user password user@domain.com [password]") print(" tools/mail.py user password user@domain.com [password]")
print(" tools/mail.py user quota user@domain [new-quota]")
print(" tools/mail.py user remove user@domain.com") print(" tools/mail.py user remove user@domain.com")
print(" tools/mail.py user make-admin user@domain.com") print(" tools/mail.py user make-admin user@domain.com")
print(" tools/mail.py user remove-admin user@domain.com") print(" tools/mail.py user remove-admin user@domain.com")
@ -81,6 +83,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"):
@ -116,6 +122,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] == "alias" and len(sys.argv) == 2: elif sys.argv[1] == "alias" and len(sys.argv) == 2:
print(mgmt("/mail/aliases")) print(mgmt("/mail/aliases"))
@ -125,6 +139,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)