mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-01 23:57:05 +00:00
commit
d64ce8c8c2
3
.github/workflows/commit-tests.yml
vendored
3
.github/workflows/commit-tests.yml
vendored
@ -33,8 +33,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
env:
|
env:
|
||||||
PRIMARY_HOSTNAME: box2.abc.com
|
PRIMARY_HOSTNAME: box2.abc.com
|
||||||
# TODO: change UPSTREAM_TAG to 'main' once upstream is installable
|
UPSTREAM_TAG: main
|
||||||
UPSTREAM_TAG: v67
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: setup
|
- name: setup
|
||||||
|
@ -18,12 +18,12 @@ objectIdentifier MiabLDAPmail MiabLDAProot:2
|
|||||||
objectIdentifier MiabLDAPmailAttributeType MiabLDAPmail:1
|
objectIdentifier MiabLDAPmailAttributeType MiabLDAPmail:1
|
||||||
objectIdentifier MiabLDAPmailObjectClass MiabLDAPmail:2
|
objectIdentifier MiabLDAPmailObjectClass MiabLDAPmail:2
|
||||||
|
|
||||||
attributetype ( 1.3.6.1.4.1.15347.2.102
|
attributetype ( 1.3.6.1.4.1.15347.2.102
|
||||||
NAME 'transport'
|
NAME 'transport'
|
||||||
SUP name)
|
SUP name)
|
||||||
|
|
||||||
attributetype ( 1.3.6.1.4.1.15347.2.101
|
attributetype ( 1.3.6.1.4.1.15347.2.101
|
||||||
NAME 'mailRoutingAddress'
|
NAME 'mailRoutingAddress'
|
||||||
SUP mail )
|
SUP mail )
|
||||||
|
|
||||||
attributetype ( 1.3.6.1.4.1.15347.2.110 NAME 'maildest'
|
attributetype ( 1.3.6.1.4.1.15347.2.110 NAME 'maildest'
|
||||||
@ -56,13 +56,74 @@ attributetype ( MiabLDAPmailAttributeType:1 NAME 'mailMember' DESC 'RFC6532 utf8
|
|||||||
# create a utf8 version of core 'domainComponent'
|
# create a utf8 version of core 'domainComponent'
|
||||||
attributetype ( MiabLDAPmailAttributeType:2 NAME 'dcIntl' DESC 'UTF8 domain component' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
|
attributetype ( MiabLDAPmailAttributeType:2 NAME 'dcIntl' DESC 'UTF8 domain component' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
|
||||||
|
|
||||||
|
# Create mda/lda user mailbox quota settings (for dovecot)
|
||||||
|
# format: number | number 'B' | number 'K' | number 'M' | number 'G'
|
||||||
|
#
|
||||||
|
# Dovecot supports more than one quota rule (but no way to use a
|
||||||
|
# multi-valued attribute). Also add additional attributes for
|
||||||
|
# more quota rules even though we're not necessarily
|
||||||
|
# using them because we might in the future which could help avoid a
|
||||||
|
# schema update. Dovecot supports "as many quota rules as you want"
|
||||||
|
|
||||||
|
attributetype ( MiabLDAPmailAttributeType:3
|
||||||
|
DESC 'MDA/LDA user mailbox quota'
|
||||||
|
NAME 'mailboxQuota'
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
|
||||||
|
EQUALITY caseExactMatch )
|
||||||
|
|
||||||
|
attributetype ( MiabLDAPmailAttributeType:4
|
||||||
|
DESC 'MDA/LDA user mailbox quota 2'
|
||||||
|
NAME 'mailboxQuota2'
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
|
||||||
|
EQUALITY caseExactMatch )
|
||||||
|
|
||||||
|
attributetype ( MiabLDAPmailAttributeType:5
|
||||||
|
DESC 'MDA/LDA user mailbox quota 3'
|
||||||
|
NAME 'mailboxQuota3'
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
|
||||||
|
EQUALITY caseExactMatch )
|
||||||
|
|
||||||
|
attributetype ( MiabLDAPmailAttributeType:6
|
||||||
|
DESC 'MDA/LDA user mailbox quota 4'
|
||||||
|
NAME 'mailboxQuota4'
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
|
||||||
|
EQUALITY caseExactMatch )
|
||||||
|
|
||||||
|
attributetype ( MiabLDAPmailAttributeType:7
|
||||||
|
DESC 'MDA/LDA user mailbox quota 5'
|
||||||
|
NAME 'mailboxQuota5'
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
|
||||||
|
EQUALITY caseExactMatch )
|
||||||
|
|
||||||
|
attributetype ( MiabLDAPmailAttributeType:8
|
||||||
|
DESC 'MDA/LDA user mailbox quota 6'
|
||||||
|
NAME 'mailboxQuota6'
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
|
||||||
|
EQUALITY caseExactMatch )
|
||||||
|
|
||||||
|
# Dovecot can maintain a flag indicating whether a user is over or
|
||||||
|
# under quota. It's use is not required, but enables postfix to reject
|
||||||
|
# messages without queuing them when a mailbox is full. The value
|
||||||
|
# should be dovecot boolean value 'yes', or 'no'.
|
||||||
|
|
||||||
|
attributetype ( MiabLDAPmailAttributeType:9
|
||||||
|
DESC 'MDA/LDA over quota flag'
|
||||||
|
NAME 'mailboxOverQuotaFlag'
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
|
||||||
|
EQUALITY caseIgnoreMatch )
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# object classes
|
||||||
|
#
|
||||||
|
|
||||||
objectclass ( 1.3.6.1.4.1.15347.2.1
|
objectclass ( 1.3.6.1.4.1.15347.2.1
|
||||||
NAME 'mailUser'
|
NAME 'mailUser'
|
||||||
DESC 'E-Mail User'
|
DESC 'E-Mail User'
|
||||||
SUP top
|
SUP top
|
||||||
AUXILIARY
|
AUXILIARY
|
||||||
MUST ( uid $ mail $ maildrop )
|
MUST ( uid $ mail $ maildrop )
|
||||||
MAY ( cn $ mailbox $ maildest $ mailaccess )
|
MAY ( cn $ mailbox $ maildest $ mailaccess $ mailboxQuota $ mailboxQuota2 $ mailboxQuota3 $ mailboxQuota4 $ mailboxQuota5 $ mailboxQuota6 $ mailboxOverQuotaFlag )
|
||||||
)
|
)
|
||||||
|
|
||||||
objectclass ( 1.3.6.1.4.1.15347.2.2
|
objectclass ( 1.3.6.1.4.1.15347.2.2
|
||||||
|
@ -74,6 +74,7 @@ if len(sys.argv) < 2:
|
|||||||
{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] (get or set user 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)
|
||||||
@ -97,6 +98,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"}:
|
||||||
@ -126,6 +131,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:
|
||||||
|
# Get 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)
|
||||||
@ -150,4 +163,3 @@ elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
|
|||||||
else:
|
else:
|
||||||
print("Invalid command-line arguments.")
|
print("Invalid command-line arguments.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ import auth, utils
|
|||||||
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, set_mail_display_name, remove_mail_user
|
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, set_mail_display_name, 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
|
||||||
from mfa import get_public_mfa_state, enable_mfa, disable_mfa
|
from mfa import get_public_mfa_state, enable_mfa, disable_mfa
|
||||||
import mfa_totp
|
import mfa_totp
|
||||||
import contextlib
|
import contextlib
|
||||||
@ -201,8 +202,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', '0')
|
||||||
try:
|
try:
|
||||||
return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), request.form.get('display_name', ''), env)
|
return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), quota, request.form.get('display_name', ''), 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)
|
||||||
|
|
||||||
|
@ -19,8 +19,11 @@
|
|||||||
# Python 3 in setup/questions.sh to validate the email
|
# Python 3 in setup/questions.sh to validate the email
|
||||||
# address entered by the user.
|
# address entered by the user.
|
||||||
|
|
||||||
import subprocess, shutil, os, sqlite3, re, ldap3, uuid, hashlib
|
import os, sqlite3, re
|
||||||
import utils, backend
|
import subprocess
|
||||||
|
import ldap3, uuid, hashlib, backend
|
||||||
|
|
||||||
|
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
|
||||||
import socket
|
import socket
|
||||||
@ -283,6 +286,30 @@ def get_mail_users(env, as_map=False, map_by="maildrop"):
|
|||||||
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 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.
|
||||||
@ -307,20 +334,52 @@ 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)
|
||||||
response = c.wait( c.search(env.LDAP_USERS_BASE, "(objectClass=mailUser)", attributes=['mail','maildrop','mailaccess','cn']) )
|
response = c.wait( c.search(env.LDAP_USERS_BASE, "(objectClass=mailUser)", attributes=['mail','maildrop','mailaccess','mailboxQuota','cn']) )
|
||||||
|
|
||||||
for rec in response:
|
for rec in response:
|
||||||
#email = rec['maildrop'][0]
|
#email = rec['maildrop'][0]
|
||||||
email = rec['mail'][0]
|
email = rec['mail'][0]
|
||||||
privileges = rec['mailaccess']
|
privileges = rec['mailaccess']
|
||||||
|
quota = rec['mailboxQuota'][0] if len(rec['mailboxQuota'])>0 else '0'
|
||||||
display_name = rec['cn'][0]
|
display_name = rec['cn'][0]
|
||||||
active_accounts.add(email)
|
active_accounts.add(email)
|
||||||
|
|
||||||
|
(user, domain) = email.split('@')
|
||||||
|
box_size = 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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
percent = (box_size / box_quota) * 100
|
||||||
|
except:
|
||||||
|
percent = 'Error'
|
||||||
|
|
||||||
|
except:
|
||||||
|
box_size = '?'
|
||||||
|
box_quota = '?'
|
||||||
|
percent = '?'
|
||||||
|
|
||||||
|
if quota == '0':
|
||||||
|
percent = ''
|
||||||
|
|
||||||
user = {
|
user = {
|
||||||
"email": email,
|
"email": email,
|
||||||
"privileges": privileges,
|
"privileges": privileges,
|
||||||
|
"quota": quota,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"display_name": display_name
|
"display_name": display_name,
|
||||||
|
"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,
|
||||||
}
|
}
|
||||||
|
|
||||||
users.append(user)
|
users.append(user)
|
||||||
|
|
||||||
# Add in archived accounts.
|
# Add in archived accounts.
|
||||||
@ -337,7 +396,10 @@ def get_mail_users_ex(env, with_archived=False):
|
|||||||
"privileges": [],
|
"privileges": [],
|
||||||
"status": "inactive",
|
"status": "inactive",
|
||||||
"mailbox": mbox,
|
"mailbox": mbox,
|
||||||
"display_name": ""
|
"display_name": "",
|
||||||
|
"box_size": '?',
|
||||||
|
"box_quota": '?',
|
||||||
|
"percent": '?',
|
||||||
}
|
}
|
||||||
users.append(user)
|
users.append(user)
|
||||||
|
|
||||||
@ -697,12 +759,13 @@ def remove_mail_domain(env, domain_idna, validate=True):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def add_mail_user(email, pw, privs, display_name, env):
|
def add_mail_user(email, pw, privs, quota, display_name, env):
|
||||||
# Add a new mail user.
|
# Add a new mail user.
|
||||||
#
|
#
|
||||||
# email: the new user's email address (idna)
|
# email: the new user's email address (idna)
|
||||||
# pw: the new user's password
|
# pw: the new user's password
|
||||||
# privs: either an array of privilege strings, or a newline
|
# privs: either an array of privilege strings, or a newline
|
||||||
|
# quota: a string (number | number 'M' | number 'G') or None
|
||||||
# separated string of privilege names
|
# separated string of privilege names
|
||||||
# display_name: a string with users givenname and surname (eg "Al Woods")
|
# display_name: a string with users givenname and surname (eg "Al Woods")
|
||||||
#
|
#
|
||||||
@ -735,6 +798,22 @@ def add_mail_user(email, pw, privs, display_name, env):
|
|||||||
validation = validate_privilege(p)
|
validation = validate_privilege(p)
|
||||||
if validation: return validation
|
if validation: return validation
|
||||||
|
|
||||||
|
if quota is None:
|
||||||
|
quota = '0'
|
||||||
|
|
||||||
|
try:
|
||||||
|
quota = validate_quota(quota)
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
|
||||||
|
if quota is None:
|
||||||
|
quota = '0'
|
||||||
|
|
||||||
|
try:
|
||||||
|
quota = validate_quota(quota)
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
|
||||||
# get the database
|
# get the database
|
||||||
conn = open_database(env)
|
conn = open_database(env)
|
||||||
|
|
||||||
@ -773,6 +852,7 @@ def add_mail_user(email, pw, privs, display_name, env):
|
|||||||
"maildrop" : email.lower(),
|
"maildrop" : email.lower(),
|
||||||
"uid" : uid,
|
"uid" : uid,
|
||||||
"mailaccess": privs,
|
"mailaccess": privs,
|
||||||
|
"mailboxQuota": quota,
|
||||||
"cn": cn,
|
"cn": cn,
|
||||||
"sn": sn,
|
"sn": sn,
|
||||||
"shadowLastChange": backend.get_shadowLastChanged()
|
"shadowLastChange": backend.get_shadowLastChanged()
|
||||||
@ -805,6 +885,10 @@ def add_mail_user(email, pw, privs, display_name, env):
|
|||||||
# convert alias's mailMember to member
|
# convert alias's mailMember to member
|
||||||
convert_mailMember(env, conn, dn, email)
|
convert_mailMember(env, conn, dn, email)
|
||||||
|
|
||||||
|
dovecot_quota_recalc(email)
|
||||||
|
|
||||||
|
dovecot_quota_recalc(email)
|
||||||
|
|
||||||
# Update things in case any new domains are added.
|
# Update things in case any new domains are added.
|
||||||
if domain_added:
|
if domain_added:
|
||||||
return kick(env, return_status)
|
return kick(env, return_status)
|
||||||
@ -866,6 +950,51 @@ def validate_login(email, pw, env):
|
|||||||
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
|
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
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]+[GMK]?$', quota):
|
||||||
|
raise ValueError("Invalid quota.")
|
||||||
|
|
||||||
|
return quota
|
||||||
|
|
||||||
|
def get_mail_quota(email, env):
|
||||||
|
user = find_mail_user(env, email, ['mailboxQuota'])
|
||||||
|
if user is None:
|
||||||
|
return ("That's not a user (%s)." % email, 400)
|
||||||
|
if len(user['mailboxQuota'])==0:
|
||||||
|
return '0'
|
||||||
|
else:
|
||||||
|
return user['mailboxQuota'][0]
|
||||||
|
|
||||||
|
def set_mail_quota(email, quota, env):
|
||||||
|
# validate that password is acceptable
|
||||||
|
quota = validate_quota(quota)
|
||||||
|
|
||||||
|
# update the database
|
||||||
|
conn = open_database(env)
|
||||||
|
user = find_mail_user(env, email, ['mailboxQuota'], conn)
|
||||||
|
if user is None:
|
||||||
|
return ("That's not a user (%s)." % email, 400)
|
||||||
|
|
||||||
|
conn.modify_record(user, { 'mailboxQuota': quota })
|
||||||
|
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_mail_password(email, env):
|
def get_mail_password(email, env):
|
||||||
# Gets the hashed passwords for a user. In ldap, userPassword is
|
# Gets the hashed passwords for a user. In ldap, userPassword is
|
||||||
@ -1329,12 +1458,12 @@ def remove_mail_alias(address_utf8, env, do_kick=True, auto=None, ignore_if_not_
|
|||||||
return return_status
|
return return_status
|
||||||
|
|
||||||
|
|
||||||
def add_auto_aliases(aliases, env):
|
# def add_auto_aliases(aliases, env):
|
||||||
conn, c = open_database(env, with_connection=True)
|
# conn, c = open_database(env, with_connection=True)
|
||||||
c.execute("DELETE FROM auto_aliases")
|
# c.execute("DELETE FROM auto_aliases")
|
||||||
for source, destination in aliases.items():
|
# for source, destination in aliases.items():
|
||||||
c.execute("INSERT INTO auto_aliases (source, destination) VALUES (?, ?)", (source, destination))
|
# c.execute("INSERT INTO auto_aliases (source, destination) VALUES (?, ?)", (source, destination))
|
||||||
conn.commit()
|
# conn.commit()
|
||||||
|
|
||||||
def get_system_administrator(env):
|
def get_system_administrator(env):
|
||||||
return "administrator@" + env['PRIMARY_HOSTNAME']
|
return "administrator@" + env['PRIMARY_HOSTNAME']
|
||||||
|
@ -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>
|
||||||
@ -34,8 +35,12 @@
|
|||||||
<div>Display Name</div>
|
<div>Display Name</div>
|
||||||
<input id="adduserDisplayName" class="form-control" type="text" placeholder="eg: John Smith">
|
<input id="adduserDisplayName" class="form-control" type="text" placeholder="eg: John Smith">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div>Quota</div>
|
||||||
|
<input type="text" class="form-control" id="adduserQuota" placeholder="Quota" style="width:5em;" value="0">
|
||||||
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div> </div>
|
<div> </div>
|
||||||
<button type="submit" class="btn btn-primary">Add User</button>
|
<button type="submit" class="btn btn-primary">Add User</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -44,13 +49,17 @@
|
|||||||
<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">Size</th>
|
||||||
|
<th class="row-center">Used</th>
|
||||||
|
<th class="row-center">Quota</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -64,10 +73,21 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="address"></span> <span class="display_name_wrapper">(<a class="display_name" href="#" onclick="users_set_displayname(this); return false;" title="Change display name"></a>)</span>
|
<span class="address"></span> <span class="display_name_wrapper">(<a class="display_name" href="#" onclick="users_set_displayname(this); return false;" title="Change display name"></a>)</span>
|
||||||
</td>
|
</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
|
||||||
@ -108,10 +128,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>
|
||||||
@ -144,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);
|
||||||
|
|
||||||
@ -162,7 +200,14 @@ 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-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);
|
||||||
if (user.status == "inactive") {
|
if (user.status == "inactive") {
|
||||||
n.find('.display_name_wrapper').text('[archived]');
|
n.find('.display_name_wrapper').text('[archived]');
|
||||||
}
|
}
|
||||||
@ -197,6 +242,7 @@ 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();
|
||||||
var display_name = $("#adduserDisplayName").val();
|
var display_name = $("#adduserDisplayName").val();
|
||||||
api(
|
api(
|
||||||
"/mail/users/add",
|
"/mail/users/add",
|
||||||
@ -205,6 +251,7 @@ function do_add_user() {
|
|||||||
email: email,
|
email: email,
|
||||||
password: pw,
|
password: pw,
|
||||||
privileges: privs,
|
privileges: privs,
|
||||||
|
quota: quota,
|
||||||
display_name: display_name
|
display_name: display_name
|
||||||
},
|
},
|
||||||
function(r) {
|
function(r) {
|
||||||
@ -272,6 +319,36 @@ function users_set_displayname(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');
|
||||||
|
|
||||||
@ -337,7 +414,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>
|
||||||
|
@ -133,7 +133,7 @@ fi
|
|||||||
if [ -z "${ENCRYPTION_AT_REST:-}" ]; then
|
if [ -z "${ENCRYPTION_AT_REST:-}" ]; then
|
||||||
source ehdd/ehdd_funcs.sh || exit 1
|
source ehdd/ehdd_funcs.sh || exit 1
|
||||||
hdd_exists && ENCRYPTION_AT_REST=true
|
hdd_exists && ENCRYPTION_AT_REST=true
|
||||||
elif [ "${ENCRYPTION_AT_REST:-}" = "false" ]; then
|
elif [ "${ENCRYPTION_AT_REST:-}" = "false" ]; then
|
||||||
source ehdd/ehdd_funcs.sh || exit 1
|
source ehdd/ehdd_funcs.sh || exit 1
|
||||||
if hdd_exists; then
|
if hdd_exists; then
|
||||||
echo "Encryption-at-rest must be disabled manually"
|
echo "Encryption-at-rest must be disabled manually"
|
||||||
@ -147,4 +147,3 @@ if [ "${ENCRYPTION_AT_REST:-false}" = "true" ]; then
|
|||||||
else
|
else
|
||||||
setup/start.sh </dev/tty
|
setup/start.sh </dev/tty
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ create_miab_conf() {
|
|||||||
_add_if_missing "${prefix}_DN" "cn=$cn,$LDAP_SERVICES_BASE"
|
_add_if_missing "${prefix}_DN" "cn=$cn,$LDAP_SERVICES_BASE"
|
||||||
_add_if_missing "${prefix}_PASSWORD" "$(generate_password 64)"
|
_add_if_missing "${prefix}_PASSWORD" "$(generate_password 64)"
|
||||||
done
|
done
|
||||||
|
|
||||||
chmod 0640 "$MIAB_INTERNAL_CONF_FILE"
|
chmod 0640 "$MIAB_INTERNAL_CONF_FILE"
|
||||||
. "$MIAB_INTERNAL_CONF_FILE"
|
. "$MIAB_INTERNAL_CONF_FILE"
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ create_service_accounts() {
|
|||||||
# create service accounts. service accounts have special access
|
# create service accounts. service accounts have special access
|
||||||
# rights, generally read-only to users, aliases, and configuration
|
# rights, generally read-only to users, aliases, and configuration
|
||||||
# subtrees (see apply_access_control)
|
# subtrees (see apply_access_control)
|
||||||
|
|
||||||
local prefix dn pass
|
local prefix dn pass
|
||||||
for prefix in ${SERVICE_ACCOUNTS[*]}
|
for prefix in ${SERVICE_ACCOUNTS[*]}
|
||||||
do
|
do
|
||||||
@ -147,7 +147,7 @@ userPassword: $(slappasswd_hash "$pass")
|
|||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ install_system_packages() {
|
|||||||
# install required deb packages, generate admin credentials
|
# install required deb packages, generate admin credentials
|
||||||
# and apply them to the installation
|
# and apply them to the installation
|
||||||
create_miab_conf
|
create_miab_conf
|
||||||
|
|
||||||
# Set installation defaults to avoid interactive dialogs. See
|
# Set installation defaults to avoid interactive dialogs. See
|
||||||
# /var/lib/dpkg/info/slapd.templates for a list of what can be set
|
# /var/lib/dpkg/info/slapd.templates for a list of what can be set
|
||||||
debconf-set-selections <<EOF
|
debconf-set-selections <<EOF
|
||||||
@ -164,10 +164,10 @@ slapd slapd/domain string ${LDAP_DOMAIN}
|
|||||||
slapd slapd/password1 password ${LDAP_ADMIN_PASSWORD}
|
slapd slapd/password1 password ${LDAP_ADMIN_PASSWORD}
|
||||||
slapd slapd/password2 password ${LDAP_ADMIN_PASSWORD}
|
slapd slapd/password2 password ${LDAP_ADMIN_PASSWORD}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Install packages
|
# Install packages
|
||||||
say "Installing OpenLDAP server..."
|
say "Installing OpenLDAP server..."
|
||||||
|
|
||||||
# we must install slapd without DEBIAN_FRONTEND=noninteractive or
|
# we must install slapd without DEBIAN_FRONTEND=noninteractive or
|
||||||
# debconf selections are ignored
|
# debconf selections are ignored
|
||||||
hide_output apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" install slapd
|
hide_output apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" install slapd
|
||||||
@ -227,7 +227,7 @@ EOF
|
|||||||
say " is set to: $ATTR_VALUE"
|
say " is set to: $ATTR_VALUE"
|
||||||
say " expected : $LDAP_ADMIN_DN"
|
say " expected : $LDAP_ADMIN_DN"
|
||||||
die
|
die
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
relocate_slapd_data() {
|
relocate_slapd_data() {
|
||||||
@ -277,11 +277,11 @@ relocate_slapd_data() {
|
|||||||
say_verbose " DB='${DB_DIR}'"
|
say_verbose " DB='${DB_DIR}'"
|
||||||
say_verbose " to:"
|
say_verbose " to:"
|
||||||
say_verbose " CONF=${MIAB_SLAPD_CONF}"
|
say_verbose " CONF=${MIAB_SLAPD_CONF}"
|
||||||
say_verbose " DB=${MIAB_SLAPD_DB_DIR}"
|
say_verbose " DB=${MIAB_SLAPD_DB_DIR}"
|
||||||
say_verbose ""
|
say_verbose ""
|
||||||
say_verbose "Stopping slapd"
|
say_verbose "Stopping slapd"
|
||||||
systemctl stop slapd || die "Could not stop slapd"
|
systemctl stop slapd || die "Could not stop slapd"
|
||||||
|
|
||||||
# Modify the path to dc=mailinabox's database directory
|
# Modify the path to dc=mailinabox's database directory
|
||||||
say_verbose "Dump config database"
|
say_verbose "Dump config database"
|
||||||
local TMP="/tmp/miab_relocate_ldap.ldif"
|
local TMP="/tmp/miab_relocate_ldap.ldif"
|
||||||
@ -320,7 +320,7 @@ schema_to_ldif() {
|
|||||||
cat="curl -s"
|
cat="curl -s"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat >"$ldif" <<EOF
|
cat >"$ldif" <<EOF
|
||||||
dn: cn=$cn,cn=schema,cn=config
|
dn: cn=$cn,cn=schema,cn=config
|
||||||
objectClass: olcSchemaConfig
|
objectClass: olcSchemaConfig
|
||||||
@ -358,7 +358,7 @@ EOF
|
|||||||
|
|
||||||
|
|
||||||
add_schemas() {
|
add_schemas() {
|
||||||
# Add necessary schema's for MiaB operaion
|
# Add necessary schema's for MiaB operaion
|
||||||
#
|
#
|
||||||
# Note: the postfix schema originally came from the ldapadmin
|
# Note: the postfix schema originally came from the ldapadmin
|
||||||
# project (GPL)(*), but has been modified to support the needs of
|
# project (GPL)(*), but has been modified to support the needs of
|
||||||
@ -385,7 +385,7 @@ add_schemas() {
|
|||||||
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
|
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
|
||||||
rm -f "$ldif"
|
rm -f "$ldif"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -481,7 +481,7 @@ EOF
|
|||||||
add_overlays() {
|
add_overlays() {
|
||||||
# Apply slapd overlays - apply the commonly used member-of overlay
|
# Apply slapd overlays - apply the commonly used member-of overlay
|
||||||
# now because adding it later is harder.
|
# now because adding it later is harder.
|
||||||
|
|
||||||
# Get the config dn for the database
|
# Get the config dn for the database
|
||||||
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
|
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
|
||||||
[ -z "$ATTR_DN" ] &&
|
[ -z "$ATTR_DN" ] &&
|
||||||
@ -498,7 +498,7 @@ add: olcModuleLoad
|
|||||||
olcModuleLoad: memberof.la
|
olcModuleLoad: memberof.la
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
get_attribute "$cdn" "(olcOverlay=memberof)" "olcOverlay"
|
get_attribute "$cdn" "(olcOverlay=memberof)" "olcOverlay"
|
||||||
if [ -z "$ATTR_DN" ]; then
|
if [ -z "$ATTR_DN" ]; then
|
||||||
say_verbose "Adding memberof overlay to $LDAP_BASE"
|
say_verbose "Adding memberof overlay to $LDAP_BASE"
|
||||||
@ -516,7 +516,7 @@ EOF
|
|||||||
|
|
||||||
add_indexes() {
|
add_indexes() {
|
||||||
# Index mail-related attributes
|
# Index mail-related attributes
|
||||||
|
|
||||||
# Get the config dn for the database
|
# Get the config dn for the database
|
||||||
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
|
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
|
||||||
[ -z "$ATTR_DN" ] &&
|
[ -z "$ATTR_DN" ] &&
|
||||||
@ -678,7 +678,7 @@ EOF
|
|||||||
#
|
#
|
||||||
process_cmdline() {
|
process_cmdline() {
|
||||||
[ -e "$MIAB_INTERNAL_CONF_FILE" ] && . "$MIAB_INTERNAL_CONF_FILE"
|
[ -e "$MIAB_INTERNAL_CONF_FILE" ] && . "$MIAB_INTERNAL_CONF_FILE"
|
||||||
|
|
||||||
if [ "$1" == "-d" ]; then
|
if [ "$1" == "-d" ]; then
|
||||||
# Start slapd in interactive/debug mode
|
# Start slapd in interactive/debug mode
|
||||||
echo "!! SERVER DEBUG MODE !!"
|
echo "!! SERVER DEBUG MODE !!"
|
||||||
@ -688,7 +688,7 @@ process_cmdline() {
|
|||||||
echo "Listening on $SLAPD_SERVICES..."
|
echo "Listening on $SLAPD_SERVICES..."
|
||||||
/usr/sbin/slapd -h "$SLAPD_SERVICES" -g openldap -u openldap -F $MIAB_SLAPD_CONF -d ${2:-1}
|
/usr/sbin/slapd -h "$SLAPD_SERVICES" -g openldap -u openldap -F $MIAB_SLAPD_CONF -d ${2:-1}
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
elif [ "$1" == "-config" ]; then
|
elif [ "$1" == "-config" ]; then
|
||||||
# Apply a certain configuration
|
# Apply a certain configuration
|
||||||
if [ "$2" == "server" ]; then
|
if [ "$2" == "server" ]; then
|
||||||
@ -734,7 +734,7 @@ process_cmdline() {
|
|||||||
local hide_attrs="(structuralObjectClass|entryUUID|creatorsName|createTimestamp|entryCSN|modifiersName|modifyTimestamp)"
|
local hide_attrs="(structuralObjectClass|entryUUID|creatorsName|createTimestamp|entryCSN|modifiersName|modifyTimestamp)"
|
||||||
local slapcat_args=(-F "$MIAB_SLAPD_CONF" -o ldif-wrap=no)
|
local slapcat_args=(-F "$MIAB_SLAPD_CONF" -o ldif-wrap=no)
|
||||||
[ ${verbose:-0} -gt 0 ] && hide_attrs="(_____NEVERMATCHES)"
|
[ ${verbose:-0} -gt 0 ] && hide_attrs="(_____NEVERMATCHES)"
|
||||||
|
|
||||||
if [ "$s" == "all" ]; then
|
if [ "$s" == "all" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo '--------------------------------'
|
echo '--------------------------------'
|
||||||
@ -777,7 +777,7 @@ process_cmdline() {
|
|||||||
if [ "$s" == "permitted-senders" -o "$s" == "ps" ]; then
|
if [ "$s" == "permitted-senders" -o "$s" == "ps" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo '--------------------------------'
|
echo '--------------------------------'
|
||||||
local attrs=(mail member mailRoutingAddress rfc822MailMember)
|
local attrs=(mail member mailRoutingAddress mailMember)
|
||||||
[ ${verbose:-0} -gt 0 ] && attrs=()
|
[ ${verbose:-0} -gt 0 ] && attrs=()
|
||||||
debug_search "(objectClass=mailGroup)" "$LDAP_PERMITTED_SENDERS_BASE" ${attrs[@]}
|
debug_search "(objectClass=mailGroup)" "$LDAP_PERMITTED_SENDERS_BASE" ${attrs[@]}
|
||||||
fi
|
fi
|
||||||
@ -814,7 +814,7 @@ process_cmdline() {
|
|||||||
rm -f "/etc/default/slapd"
|
rm -f "/etc/default/slapd"
|
||||||
echo "Done"
|
echo "Done"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
elif [ ! -z "$1" ]; then
|
elif [ ! -z "$1" ]; then
|
||||||
echo "Invalid command line argument '$1'"
|
echo "Invalid command line argument '$1'"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -76,6 +76,32 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
|
|||||||
|
|
||||||
# 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-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:User quota
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -259,7 +259,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 unix:private/policy-spf,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 unix:private/policy-spf,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).
|
||||||
|
@ -88,8 +88,13 @@ pass_attrs = maildrop=user
|
|||||||
# Post-login information specific to the user (eg. quotas). For
|
# Post-login information specific to the user (eg. quotas). For
|
||||||
# lmtp delivery, pass_filter is not used, and postfix has already
|
# lmtp delivery, pass_filter is not used, and postfix has already
|
||||||
# rewritten the envelope using the maildrop address.
|
# rewritten the envelope using the maildrop address.
|
||||||
|
# %$ is expanded to mailboxQuota's value.
|
||||||
user_filter = (&(objectClass=mailUser)(|(mail=%u)(maildrop=%u)))
|
user_filter = (&(objectClass=mailUser)(|(mail=%u)(maildrop=%u)))
|
||||||
user_attrs = maildrop=user
|
user_attrs = maildrop=user, \
|
||||||
|
mailboxQuota=quota_rule=*:bytes=%\$, \
|
||||||
|
=quota_rule2=Trash:storage=+100M, \
|
||||||
|
=quota_rule3=Drafts:storage=+25M, \
|
||||||
|
=quota_rule4=Sent:storage=+50M
|
||||||
|
|
||||||
# Account iteration for various dovecot tools (doveadm)
|
# Account iteration for various dovecot tools (doveadm)
|
||||||
iterate_filter = (objectClass=mailUser)
|
iterate_filter = (objectClass=mailUser)
|
||||||
@ -269,3 +274,6 @@ chmod 0640 /etc/postfix/virtual-alias-maps.cf
|
|||||||
|
|
||||||
restart_service postfix
|
restart_service postfix
|
||||||
restart_service dovecot
|
restart_service dovecot
|
||||||
|
|
||||||
|
# force a recalculation of all user quotas
|
||||||
|
doveadm quota recalc -A
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3 -u
|
||||||
# -*- indent-tabs-mode: t; tab-width: 8; python-indent-offset: 8; -*-
|
# -*- indent-tabs-mode: t; tab-width: 8; python-indent-offset: 8; -*-
|
||||||
#####
|
#####
|
||||||
##### This file is part of Mail-in-a-Box-LDAP which is released under the
|
##### This file is part of Mail-in-a-Box-LDAP which is released under the
|
||||||
@ -200,6 +200,12 @@ def migration_14(env):
|
|||||||
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
|
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
|
||||||
shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"])
|
shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"])
|
||||||
|
|
||||||
|
def migration_15(env):
|
||||||
|
# Add a column to the users table to store their quota limit. Default to '0' for unlimited.
|
||||||
|
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
|
||||||
|
shell("check_call", ["sqlite3", db, "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';"])
|
||||||
|
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
|
|
||||||
@ -306,7 +312,7 @@ def migration_miabldap_2(env):
|
|||||||
return ldif.replace("rfc822MailMember: ", "mailMember: ")
|
return ldif.replace("rfc822MailMember: ", "mailMember: ")
|
||||||
# apply schema changes miabldap/1 -> miabldap/2
|
# apply schema changes miabldap/1 -> miabldap/2
|
||||||
ldap.unbind()
|
ldap.unbind()
|
||||||
print("Apply schema changes")
|
print("Apply schema changes to support utf8 email addresses")
|
||||||
m14.apply_schema_changes(env, ldapvars, ldif_change_fn)
|
m14.apply_schema_changes(env, ldapvars, ldif_change_fn)
|
||||||
# reconnect
|
# reconnect
|
||||||
ldap = connect(ldapvars)
|
ldap = connect(ldapvars)
|
||||||
@ -329,6 +335,52 @@ def migration_miabldap_2(env):
|
|||||||
|
|
||||||
ldap.unbind()
|
ldap.unbind()
|
||||||
|
|
||||||
|
def migration_miabldap_3(env):
|
||||||
|
# This migration step changes the ldap schema to support quotas
|
||||||
|
#
|
||||||
|
# possible states at this point:
|
||||||
|
# miabldap was installed and is being upgraded
|
||||||
|
# -> schema update needed
|
||||||
|
# a miab install was present and step 1 upgaded it to miabldap
|
||||||
|
# -> new schema already present
|
||||||
|
#
|
||||||
|
sys.path.append(os.path.realpath(os.path.join(os.path.dirname(__file__), "../management")))
|
||||||
|
import ldap3
|
||||||
|
from backend import connect
|
||||||
|
import migration_14 as m14
|
||||||
|
|
||||||
|
# 1. get ldap site details
|
||||||
|
ldapvars = load_env_vars_from_file(os.path.join(env["STORAGE_ROOT"], "ldap/miab_ldap.conf"), strip_quotes=True)
|
||||||
|
|
||||||
|
# connect before schema changes to ensure admin password works
|
||||||
|
ldap = connect(ldapvars)
|
||||||
|
|
||||||
|
# 2. if this is a miab -> maibldap install, the new schema is
|
||||||
|
# already in place and no schema changes are needed. however,
|
||||||
|
# if this is a miabldap/1 to miabldap/2 migration, we must
|
||||||
|
# upgrade the schema.
|
||||||
|
ret = shell("check_output", [
|
||||||
|
"ldapsearch",
|
||||||
|
"-Q",
|
||||||
|
"-Y", "EXTERNAL",
|
||||||
|
"-H", "ldapi:///",
|
||||||
|
"(&(objectClass=olcSchemaConfig)(cn={*}postfix))",
|
||||||
|
"-b", "cn=schema,cn=config",
|
||||||
|
"-o", "ldif_wrap=no",
|
||||||
|
"-LLL",
|
||||||
|
"olcObjectClasses"
|
||||||
|
])
|
||||||
|
|
||||||
|
ldap.unbind()
|
||||||
|
|
||||||
|
if "mailboxQuota" not in ret:
|
||||||
|
def ldif_change_fn(ldif):
|
||||||
|
# the schema change we're making does not require any data changes
|
||||||
|
return ldif
|
||||||
|
# apply schema changes miabldap/2 -> miabldap/3
|
||||||
|
print("Apply schema changes to support mailbox quotas")
|
||||||
|
m14.apply_schema_changes(env, ldapvars, ldif_change_fn)
|
||||||
|
|
||||||
|
|
||||||
def get_current_migration():
|
def get_current_migration():
|
||||||
ver = 0
|
ver = 0
|
||||||
@ -419,7 +471,7 @@ def run_miabldap_migrations():
|
|||||||
migration_id = 0
|
migration_id = 0
|
||||||
else:
|
else:
|
||||||
print()
|
print()
|
||||||
print("%s file doesn't exists. Skipping migration..." % (migration_id_file,))
|
print("%s file doesn't exist. Skipping migration..." % (migration_id_file,))
|
||||||
return
|
return
|
||||||
|
|
||||||
ourver = int(migration_id)
|
ourver = int(migration_id)
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
import uuid, os, sqlite3, ldap3, hashlib
|
import uuid, os, sqlite3, ldap3, hashlib
|
||||||
|
|
||||||
|
|
||||||
def add_user(env, ldapconn, search_base, users_base, domains_base, email, password, privs, totp, cn=None):
|
def add_user(env, ldapconn, search_base, users_base, domains_base, email, password, privs, quota, totp, cn=None):
|
||||||
# Add a sqlite user to ldap
|
# Add a sqlite user to ldap
|
||||||
# env are the environment variables
|
# env are the environment variables
|
||||||
# ldapconn is the bound ldap connection
|
# ldapconn is the bound ldap connection
|
||||||
@ -27,6 +27,7 @@ def add_user(env, ldapconn, search_base, users_base, domains_base, email, passwo
|
|||||||
# email is the user's email
|
# email is the user's email
|
||||||
# password is the user's current sqlite password hash
|
# password is the user's current sqlite password hash
|
||||||
# privs is an array of privilege names for the user
|
# privs is an array of privilege names for the user
|
||||||
|
# quota is the users mailbox quota (string; defaults to '0')
|
||||||
# totp contains the list of secrets, mru tokens, and labels
|
# totp contains the list of secrets, mru tokens, and labels
|
||||||
# cn is the user's common name [optional]
|
# cn is the user's common name [optional]
|
||||||
#
|
#
|
||||||
@ -45,13 +46,14 @@ def add_user(env, ldapconn, search_base, users_base, domains_base, email, passwo
|
|||||||
m = hashlib.sha1()
|
m = hashlib.sha1()
|
||||||
m.update(bytearray(email.lower(),'utf-8'))
|
m.update(bytearray(email.lower(),'utf-8'))
|
||||||
uid = m.hexdigest()
|
uid = m.hexdigest()
|
||||||
|
|
||||||
# Attributes to apply to the new ldap entry
|
# Attributes to apply to the new ldap entry
|
||||||
objectClasses = [ 'inetOrgPerson','mailUser','shadowAccount' ]
|
objectClasses = [ 'inetOrgPerson','mailUser','shadowAccount' ]
|
||||||
attrs = {
|
attrs = {
|
||||||
"mail" : email,
|
"mail" : email,
|
||||||
"maildrop" : email,
|
"maildrop" : email,
|
||||||
"uid" : uid,
|
"uid" : uid,
|
||||||
|
"mailboxQuota": quota,
|
||||||
# Openldap uses prefix {CRYPT} for all crypt(3) formats
|
# Openldap uses prefix {CRYPT} for all crypt(3) formats
|
||||||
"userPassword" : password.replace('{SHA512-CRYPT}','{CRYPT}')
|
"userPassword" : password.replace('{SHA512-CRYPT}','{CRYPT}')
|
||||||
}
|
}
|
||||||
@ -91,10 +93,10 @@ def add_user(env, ldapconn, search_base, users_base, domains_base, email, passwo
|
|||||||
attrs['totpMruToken'] = totp["mru_token"]
|
attrs['totpMruToken'] = totp["mru_token"]
|
||||||
attrs['totpMruTokenTime'] = totp["mru_token_time"]
|
attrs['totpMruTokenTime'] = totp["mru_token_time"]
|
||||||
attrs['totpLabel'] = totp["label"]
|
attrs['totpLabel'] = totp["label"]
|
||||||
|
|
||||||
# Add user
|
# Add user
|
||||||
dn = "uid=%s,%s" % (uid, users_base)
|
dn = "uid=%s,%s" % (uid, users_base)
|
||||||
|
|
||||||
print("adding user %s" % email)
|
print("adding user %s" % email)
|
||||||
ldapconn.add(dn, objectClasses, attrs)
|
ldapconn.add(dn, objectClasses, attrs)
|
||||||
|
|
||||||
@ -116,14 +118,15 @@ def create_users(env, conn, ldapconn, ldap_base, ldap_users_base, ldap_domains_b
|
|||||||
|
|
||||||
# select users
|
# select users
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT id, email, password, privileges from users")
|
c.execute("SELECT id, email, password, privileges, quota from users")
|
||||||
|
|
||||||
users = {}
|
users = {}
|
||||||
for row in c:
|
for row in c:
|
||||||
user_id=row[0]
|
user_id=row[0]
|
||||||
email=row[1]
|
email=row[1]
|
||||||
password=row[2]
|
password=row[2]
|
||||||
privs=row[3]
|
privs=row[3]
|
||||||
|
quota=row[4]
|
||||||
totp = None
|
totp = None
|
||||||
|
|
||||||
c2 = conn.cursor()
|
c2 = conn.cursor()
|
||||||
@ -143,7 +146,7 @@ def create_users(env, conn, ldapconn, ldap_base, ldap_users_base, ldap_domains_b
|
|||||||
totp["label"].append("{%s}%s" % (rowidx, row2[2] or ''))
|
totp["label"].append("{%s}%s" % (rowidx, row2[2] or ''))
|
||||||
rowidx += 1
|
rowidx += 1
|
||||||
|
|
||||||
dn = add_user(env, ldapconn, ldap_base, ldap_users_base, ldap_domains_base, email, password, privs.split("\n"), totp)
|
dn = add_user(env, ldapconn, ldap_base, ldap_users_base, ldap_domains_base, email, password, privs.split("\n"), quota, totp)
|
||||||
users[email] = dn
|
users[email] = dn
|
||||||
return users
|
return users
|
||||||
|
|
||||||
@ -164,7 +167,7 @@ def create_aliases(env, conn, ldapconn, aliases_base):
|
|||||||
cn="%s" % uuid.uuid4()
|
cn="%s" % uuid.uuid4()
|
||||||
dn="cn=%s,%s" % (cn, aliases_base)
|
dn="cn=%s,%s" % (cn, aliases_base)
|
||||||
description="Mail group %s" % alias
|
description="Mail group %s" % alias
|
||||||
|
|
||||||
if alias.startswith("postmaster@") or \
|
if alias.startswith("postmaster@") or \
|
||||||
alias.startswith("hostmaster@") or \
|
alias.startswith("hostmaster@") or \
|
||||||
alias.startswith("abuse@") or \
|
alias.startswith("abuse@") or \
|
||||||
@ -172,7 +175,7 @@ def create_aliases(env, conn, ldapconn, aliases_base):
|
|||||||
alias == "administrator@" + env['PRIMARY_HOSTNAME']:
|
alias == "administrator@" + env['PRIMARY_HOSTNAME']:
|
||||||
description = "Required alias"
|
description = "Required alias"
|
||||||
|
|
||||||
print("adding alias %s" % alias)
|
print("adding alias %s" % alias)
|
||||||
ldapconn.add(dn, ['mailGroup'], {
|
ldapconn.add(dn, ['mailGroup'], {
|
||||||
"mail": alias,
|
"mail": alias,
|
||||||
"description": description
|
"description": description
|
||||||
@ -196,7 +199,7 @@ def populate_aliases(conn, ldapconn, users_map, aliases_map):
|
|||||||
alias_dn=aliases_map[alias]
|
alias_dn=aliases_map[alias]
|
||||||
members = []
|
members = []
|
||||||
mailMembers = []
|
mailMembers = []
|
||||||
|
|
||||||
for email in row[1].split(','):
|
for email in row[1].split(','):
|
||||||
email=email.strip()
|
email=email.strip()
|
||||||
if email=="":
|
if email=="":
|
||||||
@ -207,13 +210,13 @@ def populate_aliases(conn, ldapconn, users_map, aliases_map):
|
|||||||
members.append(aliases_map[email])
|
members.append(aliases_map[email])
|
||||||
else:
|
else:
|
||||||
mailMembers.append(email)
|
mailMembers.append(email)
|
||||||
|
|
||||||
print("populate alias group %s" % alias)
|
print("populate alias group %s" % alias)
|
||||||
changes = {}
|
changes = {}
|
||||||
if len(members)>0:
|
if len(members)>0:
|
||||||
changes["member"]=[(ldap3.MODIFY_REPLACE, members)]
|
changes["member"]=[(ldap3.MODIFY_REPLACE, members)]
|
||||||
if len(mailMembers)>0:
|
if len(mailMembers)>0:
|
||||||
changes["rfc822MailMember"]=[(ldap3.MODIFY_REPLACE, mailMembers)]
|
changes["rfc822MailMember"]=[(ldap3.MODIFY_REPLACE, mailMembers)]
|
||||||
ldapconn.modify(alias_dn, changes)
|
ldapconn.modify(alias_dn, changes)
|
||||||
|
|
||||||
|
|
||||||
|
@ -214,6 +214,7 @@ cat > $RCM_CONFIG <<EOF;
|
|||||||
|
|
||||||
/* 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
|
||||||
|
|
||||||
|
@ -19,16 +19,16 @@ parse_miab_version_string() {
|
|||||||
local tmpfile
|
local tmpfile
|
||||||
tmpfile=$(mktemp)
|
tmpfile=$(mktemp)
|
||||||
awk -F- '
|
awk -F- '
|
||||||
/^v[0-9]+\./ { split($1,a,"."); print "MAJOR="substr(a[1],2); print "MINOR="a[2]; print "RELEASE="$2; next }
|
/^v[0-9]+\./ { split($1,a,"."); print "MAJOR="substr(a[1],2); print "MINOR="a[2]; print "RELEASE="$2; next }
|
||||||
|
|
||||||
$1 ~ /^v[0-9]+[a-z]$/ { print "MAJOR="substr($1,2,length($1)-2); print "MINOR="substr($1,length($1))-"a"+1; print "RELEASE="; next }
|
$1 ~ /^v[0-9]+[a-z]$/ { print "MAJOR="substr($1,2,length($1)-2); print "MINOR="substr($1,length($1))-"a"+1; print "RELEASE="; next }
|
||||||
|
|
||||||
$1 ~ /^v[0-9]+[A-Z]$/ { print "MAJOR="substr($1,2,length($1)-2); print "MINOR="substr($1,length($1))-"A"+1; print "RELEASE="; next }
|
$1 ~ /^v[0-9]+[A-Z]$/ { print "MAJOR="substr($1,2,length($1)-2); print "MINOR="substr($1,length($1))-"A"+1; print "RELEASE="; next }
|
||||||
|
|
||||||
$1 ~ /^v[0-9]+$/ { print "MAJOR="substr($1,2); print "MINOR="; print "RELEASE="; next }
|
$1 ~ /^v[0-9]+$/ { print "MAJOR="substr($1,2); print "MINOR="; print "RELEASE="; next }
|
||||||
|
|
||||||
{ exit 1 }' >> "$tmpfile" <<< "$1"
|
{ exit 1 }' >> "$tmpfile" <<< "$1"
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
rm -f "$tmpfile"
|
rm -f "$tmpfile"
|
||||||
return 1
|
return 1
|
||||||
@ -79,7 +79,7 @@ installed_state_capture() {
|
|||||||
echo "MAJOR=$MAJOR" >>"$info"
|
echo "MAJOR=$MAJOR" >>"$info"
|
||||||
echo "MINOR=$MINOR" >>"$info"
|
echo "MINOR=$MINOR" >>"$info"
|
||||||
echo "RELEASE=$RELEASE" >>"$info"
|
echo "RELEASE=$RELEASE" >>"$info"
|
||||||
|
|
||||||
echo "GIT_ORIGIN='$(git remote -v | grep ^origin | grep 'fetch)$' | awk '{print $2}')'" >>"$info"
|
echo "GIT_ORIGIN='$(git remote -v | grep ^origin | grep 'fetch)$' | awk '{print $2}')'" >>"$info"
|
||||||
echo "MIGRATION_VERSION=$([ -e "$STORAGE_ROOT/mailinabox.version" ] && cat "$STORAGE_ROOT/mailinabox.version")" >>"$info"
|
echo "MIGRATION_VERSION=$([ -e "$STORAGE_ROOT/mailinabox.version" ] && cat "$STORAGE_ROOT/mailinabox.version")" >>"$info"
|
||||||
echo "MIGRATION_ML_VERSION=$([ -e "$STORAGE_ROOT/mailinabox-ldap.version" ] && cat "$STORAGE_ROOT/mailinabox-ldap.version")" >>"$info"
|
echo "MIGRATION_ML_VERSION=$([ -e "$STORAGE_ROOT/mailinabox-ldap.version" ] && cat "$STORAGE_ROOT/mailinabox-ldap.version")" >>"$info"
|
||||||
@ -114,7 +114,7 @@ installed_state_capture() {
|
|||||||
return 3
|
return 3
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ installed_state_capture() {
|
|||||||
installed_state_compare() {
|
installed_state_compare() {
|
||||||
local s1="$1"
|
local s1="$1"
|
||||||
local s2="$2"
|
local s2="$2"
|
||||||
|
|
||||||
local output
|
local output
|
||||||
local changed="false"
|
local changed="false"
|
||||||
|
|
||||||
@ -138,13 +138,15 @@ installed_state_compare() {
|
|||||||
RELEASE_A="${RELEASE:-0}"
|
RELEASE_A="${RELEASE:-0}"
|
||||||
PROD_A="miab"
|
PROD_A="miab"
|
||||||
grep "mailinabox-ldap" <<<"$GIT_ORIGIN" >/dev/null && PROD_A="miabldap"
|
grep "mailinabox-ldap" <<<"$GIT_ORIGIN" >/dev/null && PROD_A="miabldap"
|
||||||
|
MIGRATION_ML_VERSION_A="${MIGRATION_ML_VERSION:-0}"
|
||||||
|
|
||||||
source "$s2/info.txt"
|
source "$s2/info.txt"
|
||||||
MAJOR_B="$MAJOR"
|
MAJOR_B="$MAJOR"
|
||||||
MINOR_B="${MINOR:-0}"
|
MINOR_B="${MINOR:-0}"
|
||||||
RELEASE_B="${RELEASE:-0}"
|
RELEASE_B="${RELEASE:-0}"
|
||||||
PROD_B="miab"
|
PROD_B="miab"
|
||||||
grep "mailinabox-ldap" <<<"$GIT_ORIGIN" >/dev/null && PROD_B="miabldap"
|
grep "mailinabox-ldap" <<<"$GIT_ORIGIN" >/dev/null && PROD_B="miabldap"
|
||||||
|
MIGRATION_ML_VERSION_B="${MIGRATION_ML_VERSION:-0}"
|
||||||
|
|
||||||
cmptype="${PROD_A}2${PROD_B}"
|
cmptype="${PROD_A}2${PROD_B}"
|
||||||
|
|
||||||
@ -155,7 +157,7 @@ installed_state_compare() {
|
|||||||
cp "$s1/aliases.json" "$s1/aliases-cmp.json" || changed="true"
|
cp "$s1/aliases.json" "$s1/aliases-cmp.json" || changed="true"
|
||||||
cp "$s2/users.json" "$s2/users-cmp.json" || changed="true"
|
cp "$s2/users.json" "$s2/users-cmp.json" || changed="true"
|
||||||
cp "$s2/aliases.json" "$s2/aliases-cmp.json" || changed="true"
|
cp "$s2/aliases.json" "$s2/aliases-cmp.json" || changed="true"
|
||||||
|
|
||||||
if [ "$cmptype" = "miab2miabldap" ]
|
if [ "$cmptype" = "miab2miabldap" ]
|
||||||
then
|
then
|
||||||
# user display names is a feature added to MiaB-LDAP that is
|
# user display names is a feature added to MiaB-LDAP that is
|
||||||
@ -164,7 +166,7 @@ installed_state_compare() {
|
|||||||
|
|
||||||
# alias descriptions is a feature added to MiaB-LDAP that is
|
# alias descriptions is a feature added to MiaB-LDAP that is
|
||||||
# not in MiaB
|
# not in MiaB
|
||||||
grep -v '"description":' "$s2/aliases.json" > "$s2/aliases-cmp.json" || changed="true"
|
grep -v '"description":' "$s2/aliases.json" > "$s2/aliases-cmp.json" || changed="true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# cmp: v0.54 to current
|
# cmp: v0.54 to current
|
||||||
@ -176,9 +178,16 @@ installed_state_compare() {
|
|||||||
|
|
||||||
# s2: re-sort aliases
|
# s2: re-sort aliases
|
||||||
jq -c ".[] | .aliases | sort_by(.address) | .[] | {address:.address, forwards_to:.forwards_to, permitted_senders:.permitted_senders, auto:.auto, description:.description}" "$s2/aliases.json" > "$s2/aliases-cmp.json"
|
jq -c ".[] | .aliases | sort_by(.address) | .[] | {address:.address, forwards_to:.forwards_to, permitted_senders:.permitted_senders, auto:.auto, description:.description}" "$s2/aliases.json" > "$s2/aliases-cmp.json"
|
||||||
|
|
||||||
|
if [ $MIGRATION_ML_VERSION_A -le 2 -a $MIGRATION_ML_VERSION_B -ge 3 ]; then
|
||||||
|
# miabldap migration level <=2 does not have quota fields, so
|
||||||
|
# remove them from the comparison
|
||||||
|
grep -vE '"(quota|box_quota|box_size|percent)":' "$s2/users-cmp.json" > "$s2/users-cmp2.json" || changed="true"
|
||||||
|
cp "$s2/users-cmp2.json" "$s2/users-cmp.json" && rm -f "$s2/users-cmp2.json"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# users
|
# users
|
||||||
#
|
#
|
||||||
@ -195,7 +204,7 @@ installed_state_compare() {
|
|||||||
#
|
#
|
||||||
# aliases
|
# aliases
|
||||||
#
|
#
|
||||||
|
|
||||||
H2 "Aliases"
|
H2 "Aliases"
|
||||||
output="$(diff "$s1/aliases-cmp.json" "$s2/aliases-cmp.json" 2>&1)"
|
output="$(diff "$s1/aliases-cmp.json" "$s2/aliases-cmp.json" 2>&1)"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@ -241,13 +250,13 @@ installed_state_compare() {
|
|||||||
# $4 == "SOA" { print $1" "$3" "$4" "$5" "$6" "$8" "$10" "$12; next } \
|
# $4 == "SOA" { print $1" "$3" "$4" "$5" "$6" "$8" "$10" "$12; next } \
|
||||||
# { for(i=1;i<=NF;i++) if (i!=2) printf("%s ",$i); print ""; }' \
|
# { for(i=1;i<=NF;i++) if (i!=2) printf("%s ",$i); print ""; }' \
|
||||||
# "$s1/zones/$zone" > "$t1"
|
# "$s1/zones/$zone" > "$t1"
|
||||||
|
|
||||||
# awk '\
|
# awk '\
|
||||||
# $4 == "RRSIG" || $4 == "NSEC3" { next; } \
|
# $4 == "RRSIG" || $4 == "NSEC3" { next; } \
|
||||||
# $4 == "SOA" { print $1" "$3" "$4" "$5" "$6" "$8" "$10" "$12; next } \
|
# $4 == "SOA" { print $1" "$3" "$4" "$5" "$6" "$8" "$10" "$12; next } \
|
||||||
# { for(i=1;i<=NF;i++) if (i!=2) printf("%s ",$i); print ""; }' \
|
# { for(i=1;i<=NF;i++) if (i!=2) printf("%s ",$i); print ""; }' \
|
||||||
# "$s2/zones/$zone" > "$t2"
|
# "$s2/zones/$zone" > "$t2"
|
||||||
|
|
||||||
# output="$(diff "$t1" "$t2" 2>&1)"
|
# output="$(diff "$t1" "$t2" 2>&1)"
|
||||||
# if [ $? -ne 0 ]; then
|
# if [ $? -ne 0 ]; then
|
||||||
# echo "CHANGED zone: $zone"
|
# echo "CHANGED zone: $zone"
|
||||||
|
@ -34,11 +34,11 @@ mgmt_start() {
|
|||||||
MGMT_ADMIN_PW="$(generate_password)"
|
MGMT_ADMIN_PW="$(generate_password)"
|
||||||
|
|
||||||
delete_user "$MGMT_ADMIN_EMAIL"
|
delete_user "$MGMT_ADMIN_EMAIL"
|
||||||
|
|
||||||
record "[Creating a new account with admin rights for management tests]"
|
record "[Creating a new account with admin rights for management tests]"
|
||||||
create_user "$MGMT_ADMIN_EMAIL" "$MGMT_ADMIN_PW" "admin"
|
create_user "$MGMT_ADMIN_EMAIL" "$MGMT_ADMIN_PW" "admin"
|
||||||
MGMT_ADMIN_DN="$ATTR_DN"
|
MGMT_ADMIN_DN="$ATTR_DN"
|
||||||
record "Created: $MGMT_ADMIN_EMAIL at $MGMT_ADMIN_DN"
|
record "Created: $MGMT_ADMIN_EMAIL at $MGMT_ADMIN_DN"
|
||||||
}
|
}
|
||||||
|
|
||||||
mgmt_end() {
|
mgmt_end() {
|
||||||
@ -191,7 +191,7 @@ mgmt_assert_privileges_add() {
|
|||||||
mgmt_get_totp_token() {
|
mgmt_get_totp_token() {
|
||||||
local secret="$1"
|
local secret="$1"
|
||||||
local mru_token="$2"
|
local mru_token="$2"
|
||||||
|
|
||||||
TOTP_TOKEN="" # this is set to the acquired token on success
|
TOTP_TOKEN="" # this is set to the acquired token on success
|
||||||
|
|
||||||
# the user would normally give the secret to an authenticator app
|
# the user would normally give the secret to an authenticator app
|
||||||
@ -202,7 +202,7 @@ mgmt_get_totp_token() {
|
|||||||
record "[Get the current token for the secret '$secret']"
|
record "[Get the current token for the secret '$secret']"
|
||||||
|
|
||||||
local count=0
|
local count=0
|
||||||
|
|
||||||
while [ -z "$TOTP_TOKEN" -a $count -lt 10 ]; do
|
while [ -z "$TOTP_TOKEN" -a $count -lt 10 ]; do
|
||||||
TOTP_TOKEN="$(totp_current_token "$secret" 2>>"$TEST_OF")"
|
TOTP_TOKEN="$(totp_current_token "$secret" 2>>"$TEST_OF")"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@ -218,13 +218,13 @@ mgmt_get_totp_token() {
|
|||||||
record "Success: token is '$TOTP_TOKEN'"
|
record "Success: token is '$TOTP_TOKEN'"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
let count+=1
|
let count+=1
|
||||||
done
|
done
|
||||||
|
|
||||||
record "Failed: timeout !"
|
record "Failed: timeout !"
|
||||||
TOTP_TOKEN=""
|
TOTP_TOKEN=""
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
mgmt_mfa_status() {
|
mgmt_mfa_status() {
|
||||||
@ -246,7 +246,7 @@ mgmt_totp_enable() {
|
|||||||
# returns 1 if a REST error occured. $REST_ERROR has the message
|
# returns 1 if a REST error occured. $REST_ERROR has the message
|
||||||
# returns 2 if some other error occured
|
# returns 2 if some other error occured
|
||||||
#
|
#
|
||||||
|
|
||||||
local user="$1"
|
local user="$1"
|
||||||
local pw="$2"
|
local pw="$2"
|
||||||
local label="$3" # optional
|
local label="$3" # optional
|
||||||
@ -258,7 +258,7 @@ mgmt_totp_enable() {
|
|||||||
if ! mgmt_mfa_status "$user" "$pw"; then
|
if ! mgmt_mfa_status "$user" "$pw"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TOTP_SECRET="$(/usr/bin/jq -r ".new_mfa.totp.secret" <<<"$REST_OUTPUT")"
|
TOTP_SECRET="$(/usr/bin/jq -r ".new_mfa.totp.secret" <<<"$REST_OUTPUT")"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
record "Unable to obtain setup totp secret - is 'jq' installed?"
|
record "Unable to obtain setup totp secret - is 'jq' installed?"
|
||||||
@ -271,11 +271,11 @@ mgmt_totp_enable() {
|
|||||||
else
|
else
|
||||||
record "Found TOTP secret '$TOTP_SECRET'"
|
record "Found TOTP secret '$TOTP_SECRET'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! mgmt_get_totp_token "$TOTP_SECRET"; then
|
if ! mgmt_get_totp_token "$TOTP_SECRET"; then
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. enable TOTP
|
# 2. enable TOTP
|
||||||
record "Enabling TOTP using the secret and token"
|
record "Enabling TOTP using the secret and token"
|
||||||
if ! mgmt_rest_as_user "POST" "/admin/mfa/totp/enable" "$user" "$pw" "secret=$TOTP_SECRET" "token=$TOTP_TOKEN" "label=$label"; then
|
if ! mgmt_rest_as_user "POST" "/admin/mfa/totp/enable" "$user" "$pw" "secret=$TOTP_SECRET" "token=$TOTP_TOKEN" "label=$label"; then
|
||||||
@ -284,7 +284,7 @@ mgmt_totp_enable() {
|
|||||||
else
|
else
|
||||||
record "Success: POST /mfa/totp/enable: '$REST_OUTPUT'"
|
record "Success: POST /mfa/totp/enable: '$REST_OUTPUT'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +318,7 @@ mgmt_mfa_disable() {
|
|||||||
local user="$1"
|
local user="$1"
|
||||||
local pw="$2"
|
local pw="$2"
|
||||||
local mfa_id="$3"
|
local mfa_id="$3"
|
||||||
|
|
||||||
record "[Disable MFA for $user]"
|
record "[Disable MFA for $user]"
|
||||||
if [ "$mfa_id" == "all" ]; then
|
if [ "$mfa_id" == "all" ]; then
|
||||||
mfa_id=""
|
mfa_id=""
|
||||||
@ -327,7 +327,7 @@ mgmt_mfa_disable() {
|
|||||||
if ! mgmt_mfa_status "$user" "$pw"; then
|
if ! mgmt_mfa_status "$user" "$pw"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mfa_id="$(/usr/bin/jq -r ".enabled_mfa[0].id" <<<"$REST_OUTPUT")"
|
mfa_id="$(/usr/bin/jq -r ".enabled_mfa[0].id" <<<"$REST_OUTPUT")"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
record "Unable to use /usr/bin/jq - is it installed?"
|
record "Unable to use /usr/bin/jq - is it installed?"
|
||||||
@ -338,9 +338,9 @@ mgmt_mfa_disable() {
|
|||||||
return 3
|
return 3
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if ! mgmt_rest_as_user "POST" "/admin/mfa/disable" "$user" "$pw" "mfa-id=$mfa_id"
|
if ! mgmt_rest_as_user "POST" "/admin/mfa/disable" "$user" "$pw" "mfa-id=$mfa_id"
|
||||||
then
|
then
|
||||||
REST_ERROR="Failed: POST /admin/mfa/disable: $REST_ERROR"
|
REST_ERROR="Failed: POST /admin/mfa/disable: $REST_ERROR"
|
||||||
@ -387,7 +387,7 @@ mgmt_assert_admin_login() {
|
|||||||
if [ $code -ne 0 ]; then
|
if [ $code -ne 0 ]; then
|
||||||
test_failure "Unable to run jq ($code) on /admin/login json"
|
test_failure "Unable to run jq ($code) on /admin/login json"
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
elif [ "$status" == "null" ]; then
|
elif [ "$status" == "null" ]; then
|
||||||
test_failure "No 'status' in /admin/login json"
|
test_failure "No 'status' in /admin/login json"
|
||||||
return 1
|
return 1
|
||||||
@ -395,8 +395,39 @@ mgmt_assert_admin_login() {
|
|||||||
elif [ "$status" != "$expected_status" ]; then
|
elif [ "$status" != "$expected_status" ]; then
|
||||||
test_failure "Expected a login status of '$expected_status', but got '$status'"
|
test_failure "Expected a login status of '$expected_status', but got '$status'"
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mgmt_get_user_quota() {
|
||||||
|
local user="$1"
|
||||||
|
record "[get user $user quota]"
|
||||||
|
mgmt_rest GET "/admin/mail/users/quota?email=$user"
|
||||||
|
local rc=$?
|
||||||
|
# REST_OUTPUT contains json, eg:
|
||||||
|
# { "email": "alice@somedomain.com", "quota": "5000" }
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
# output to stdout the quota value
|
||||||
|
QUOTA="$(/usr/bin/jq -r ".quota" <<<"$REST_OUTPUT" 2>>$TEST_OF)"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
record "could not obtain quota member from json using jq"
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
QUOTA="error"
|
||||||
|
fi
|
||||||
|
return $rc
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mgmt_set_user_quota() {
|
||||||
|
local user="$1"
|
||||||
|
local quota="$2"
|
||||||
|
record "[set user $user quota to $quota]"
|
||||||
|
mgmt_rest POST "/admin/mail/users/quota" "email=$user" "quota=$quota"
|
||||||
|
local rc=$?
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
##### details.
|
##### details.
|
||||||
#####
|
#####
|
||||||
|
|
||||||
#
|
#
|
||||||
# User management tests
|
# User management tests
|
||||||
|
|
||||||
_test_mixed_case() {
|
_test_mixed_case() {
|
||||||
@ -29,16 +29,16 @@ _test_mixed_case() {
|
|||||||
test_failure "Creation of a user with the same email address, but different case, succeeded."
|
test_failure "Creation of a user with the same email address, but different case, succeeded."
|
||||||
test_failure "${REST_ERROR}"
|
test_failure "${REST_ERROR}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# create an alias group with alice in it
|
# create an alias group with alice in it
|
||||||
mgmt_assert_create_alias_group "${aliases[0]}" "${alices[1]}"
|
mgmt_assert_create_alias_group "${aliases[0]}" "${alices[1]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# create local user bob
|
# create local user bob
|
||||||
mgmt_assert_create_user "${bobs[0]}" "$bob_pw"
|
mgmt_assert_create_user "${bobs[0]}" "$bob_pw"
|
||||||
|
|
||||||
assert_check_logs
|
assert_check_logs
|
||||||
|
|
||||||
|
|
||||||
# send mail from bob to alice
|
# send mail from bob to alice
|
||||||
#
|
#
|
||||||
@ -60,7 +60,7 @@ _test_mixed_case() {
|
|||||||
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP ${alices[3]} "$alice_pw" 2>&1)"
|
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP ${alices[3]} "$alice_pw" 2>&1)"
|
||||||
assert_python_success $? "$output"
|
assert_python_success $? "$output"
|
||||||
assert_check_logs
|
assert_check_logs
|
||||||
|
|
||||||
# send mail from alice as the alias to bob, ensure bob got it
|
# send mail from alice as the alias to bob, ensure bob got it
|
||||||
#
|
#
|
||||||
record "[Mailing to bob as alias from alice]"
|
record "[Mailing to bob as alias from alice]"
|
||||||
@ -87,7 +87,7 @@ test_mixed_case_users() {
|
|||||||
# send mail from that user as the alias to the other user
|
# send mail from that user as the alias to the other user
|
||||||
|
|
||||||
test_start "mixed-case-users"
|
test_start "mixed-case-users"
|
||||||
|
|
||||||
local alices=(alice@mgmt.somedomain.com
|
local alices=(alice@mgmt.somedomain.com
|
||||||
aLICE@mgmt.somedomain.com
|
aLICE@mgmt.somedomain.com
|
||||||
aLiCe@mgmt.somedomain.com
|
aLiCe@mgmt.somedomain.com
|
||||||
@ -102,7 +102,7 @@ test_mixed_case_users() {
|
|||||||
ALICE@mgmt.anotherdomain.com)
|
ALICE@mgmt.anotherdomain.com)
|
||||||
|
|
||||||
_test_mixed_case "${alices[*]}" "${bobs[*]}" "${aliases[*]}"
|
_test_mixed_case "${alices[*]}" "${bobs[*]}" "${aliases[*]}"
|
||||||
|
|
||||||
test_end
|
test_end
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ test_mixed_case_domains() {
|
|||||||
# send mail from that user as the alias to the other user
|
# send mail from that user as the alias to the other user
|
||||||
|
|
||||||
test_start "mixed-case-domains"
|
test_start "mixed-case-domains"
|
||||||
|
|
||||||
local alices=(alice@mgmt.somedomain.com
|
local alices=(alice@mgmt.somedomain.com
|
||||||
alice@MGMT.somedomain.com
|
alice@MGMT.somedomain.com
|
||||||
alice@mgmt.SOMEDOMAIN.com
|
alice@mgmt.SOMEDOMAIN.com
|
||||||
@ -128,9 +128,9 @@ test_mixed_case_domains() {
|
|||||||
local aliases=(alice@MGMT.anotherdomain.com
|
local aliases=(alice@MGMT.anotherdomain.com
|
||||||
alice@mgmt.ANOTHERDOMAIN.com
|
alice@mgmt.ANOTHERDOMAIN.com
|
||||||
alice@Mgmt.AnotherDomain.Com)
|
alice@Mgmt.AnotherDomain.Com)
|
||||||
|
|
||||||
_test_mixed_case "${alices[*]}" "${bobs[*]}" "${aliases[*]}"
|
_test_mixed_case "${alices[*]}" "${bobs[*]}" "${aliases[*]}"
|
||||||
|
|
||||||
test_end
|
test_end
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ test_intl_domains() {
|
|||||||
[ ! -z "$ATTR_DN" ] && record_search "$ATTR_DN"
|
[ ! -z "$ATTR_DN" ] && record_search "$ATTR_DN"
|
||||||
else
|
else
|
||||||
record_search "$ATTR_DN"
|
record_search "$ATTR_DN"
|
||||||
|
|
||||||
# required aliases are automatically created and should
|
# required aliases are automatically created and should
|
||||||
# have both mail addresses (idna and utf8)
|
# have both mail addresses (idna and utf8)
|
||||||
get_attribute "$LDAP_ALIASES_BASE" "(mail=abuse@$intl_person_idna_domain)" "mail"
|
get_attribute "$LDAP_ALIASES_BASE" "(mail=abuse@$intl_person_idna_domain)" "mail"
|
||||||
@ -191,7 +191,7 @@ test_intl_domains() {
|
|||||||
test_failure "Require alias abuse@$intl_person_idna_domain expected to contain both idna and utf8 mail addresses"
|
test_failure "Require alias abuse@$intl_person_idna_domain expected to contain both idna and utf8 mail addresses"
|
||||||
record_search "$ATTR_DN"
|
record_search "$ATTR_DN"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ensure user is removed as is expected by the remaining tests
|
# ensure user is removed as is expected by the remaining tests
|
||||||
mgmt_delete_user "$intl_person_idna"
|
mgmt_delete_user "$intl_person_idna"
|
||||||
fi
|
fi
|
||||||
@ -204,7 +204,7 @@ test_intl_domains() {
|
|||||||
test_failure "No required alias should not exist for the $intl_person_domain domain"
|
test_failure "No required alias should not exist for the $intl_person_domain domain"
|
||||||
record_search "$ATTR_DN"
|
record_search "$ATTR_DN"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# create local users bob and mary
|
# create local users bob and mary
|
||||||
mgmt_assert_create_user "$bob" "$bob_pw"
|
mgmt_assert_create_user "$bob" "$bob_pw"
|
||||||
mgmt_assert_create_user "$mary" "$mary_pw"
|
mgmt_assert_create_user "$mary" "$mary_pw"
|
||||||
@ -212,7 +212,7 @@ test_intl_domains() {
|
|||||||
# create intl alias with local user bob and intl_person in it
|
# create intl alias with local user bob and intl_person in it
|
||||||
if mgmt_assert_create_alias_group "$alias" "$bob" "$intl_person"; then
|
if mgmt_assert_create_alias_group "$alias" "$bob" "$intl_person"; then
|
||||||
# examine LDAP server to verify IDNA-encodings
|
# examine LDAP server to verify IDNA-encodings
|
||||||
|
|
||||||
# 1. the mail attribute for the alias should have both the
|
# 1. the mail attribute for the alias should have both the
|
||||||
# idna and utf8 addresses
|
# idna and utf8 addresses
|
||||||
get_attribute "$LDAP_ALIASES_BASE" "(mail=$alias)" "mail"
|
get_attribute "$LDAP_ALIASES_BASE" "(mail=$alias)" "mail"
|
||||||
@ -238,7 +238,7 @@ test_intl_domains() {
|
|||||||
|
|
||||||
# re-create intl alias with local user bob only
|
# re-create intl alias with local user bob only
|
||||||
mgmt_assert_create_alias_group "$alias" "$bob"
|
mgmt_assert_create_alias_group "$alias" "$bob"
|
||||||
|
|
||||||
assert_check_logs
|
assert_check_logs
|
||||||
|
|
||||||
if ! have_test_failures; then
|
if ! have_test_failures; then
|
||||||
@ -300,18 +300,18 @@ test_totp() {
|
|||||||
record "Expect a login failure..."
|
record "Expect a login failure..."
|
||||||
mgmt_assert_admin_login "$alice" "$alice_pw" "missing-totp-token"
|
mgmt_assert_admin_login "$alice" "$alice_pw" "missing-totp-token"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# logging into /admin/me with a password and a token should
|
# logging into /admin/me with a password and a token should
|
||||||
# succeed, and an api_key generated
|
# succeed, and an api_key generated
|
||||||
local api_key
|
local api_key
|
||||||
if ! have_test_failures; then
|
if ! have_test_failures; then
|
||||||
record "Try using a password and a token to get the user api key, we may have to wait 30 seconds to get a new token..."
|
record "Try using a password and a token to get the user api key, we may have to wait 30 seconds to get a new token..."
|
||||||
|
|
||||||
local old_totp_token="$TOTP_TOKEN"
|
local old_totp_token="$TOTP_TOKEN"
|
||||||
if ! mgmt_get_totp_token "$TOTP_SECRET" "$TOTP_TOKEN"; then
|
if ! mgmt_get_totp_token "$TOTP_SECRET" "$TOTP_TOKEN"; then
|
||||||
test_failure "Could not obtain a new TOTP token"
|
test_failure "Could not obtain a new TOTP token"
|
||||||
|
|
||||||
else
|
else
|
||||||
# we have a new token, try logging in ...
|
# we have a new token, try logging in ...
|
||||||
# the token must be placed in the header "x-auth-token"
|
# the token must be placed in the header "x-auth-token"
|
||||||
@ -331,7 +331,7 @@ test_totp() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# we should be able to login using the user's api key
|
# we should be able to login using the user's api key
|
||||||
if ! have_test_failures; then
|
if ! have_test_failures; then
|
||||||
record "[Use the session key to enum users]"
|
record "[Use the session key to enum users]"
|
||||||
if ! mgmt_rest_as_user "GET" "/admin/mail/users?format=json" "$alice" "$api_key"; then
|
if ! mgmt_rest_as_user "GET" "/admin/mail/users?format=json" "$alice" "$api_key"; then
|
||||||
test_failure "Unable to use the session key to issue a rest call: $REST_ERROR"
|
test_failure "Unable to use the session key to issue a rest call: $REST_ERROR"
|
||||||
@ -342,7 +342,7 @@ test_totp() {
|
|||||||
|
|
||||||
# disable totp on the account - login should work with just the password
|
# disable totp on the account - login should work with just the password
|
||||||
# and the ldap entry should not have the 'totpUser' objectClass
|
# and the ldap entry should not have the 'totpUser' objectClass
|
||||||
if ! have_test_failures; then
|
if ! have_test_failures; then
|
||||||
if mgmt_assert_mfa_disable "$alice" "$api_key"; then
|
if mgmt_assert_mfa_disable "$alice" "$api_key"; then
|
||||||
mgmt_assert_admin_login "$alice" "$alice_pw" "ok"
|
mgmt_assert_admin_login "$alice" "$alice_pw" "ok"
|
||||||
fi
|
fi
|
||||||
@ -354,19 +354,130 @@ test_totp() {
|
|||||||
else
|
else
|
||||||
check_logs
|
check_logs
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mgmt_assert_delete_user "$alice"
|
mgmt_assert_delete_user "$alice"
|
||||||
test_end
|
test_end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test_mailbox_quotas() {
|
||||||
|
test_start "mailbox-quotas"
|
||||||
|
|
||||||
|
# create standard user alice
|
||||||
|
local alice="alice@somedomain.com"
|
||||||
|
create_user "$alice" "alice"
|
||||||
|
|
||||||
|
# quota should be unlimited for newly added users
|
||||||
|
if ! mgmt_get_user_quota "$alice"; then
|
||||||
|
test_failure "Unable to get $alice's quota: $REST_ERROR"
|
||||||
|
elif [ "$QUOTA" != "0" -a "$QUOTA" != "unlimited" ]; then
|
||||||
|
test_failure "A newly created user should have unlimited quota"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# get alice's current total number of messages. should be 0 unless
|
||||||
|
# the account was "archived"
|
||||||
|
local count_messages="$(doveadm -f json quota get -u "$alice" | jq -r '.[] | select(.type=="MESSAGE") | .value')"
|
||||||
|
record "$alice currently has $count_messages messages"
|
||||||
|
|
||||||
|
# set alice's quota to a small number
|
||||||
|
local quota_value="5K"
|
||||||
|
if ! mgmt_set_user_quota "$alice" "$quota_value"
|
||||||
|
then
|
||||||
|
test_failure "Unable to set $alice's quota: $REST_ERROR"
|
||||||
|
else
|
||||||
|
# read back the quota - make sure it's what we set
|
||||||
|
if ! mgmt_get_user_quota "$alice" || [ "$QUOTA" != "$quota_value" ]
|
||||||
|
then
|
||||||
|
test_failure "Setting quota failed - expected quota does not match current quota: $REST_OUTPUT $REST_ERROR QUOTA=$QUOTA"
|
||||||
|
|
||||||
|
else
|
||||||
|
record_search "(mail=$alice)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! have_test_failures; then
|
||||||
|
# send messages large enough to exceed the quota
|
||||||
|
local output
|
||||||
|
local subjects=()
|
||||||
|
local msgidx=0
|
||||||
|
local body="$(python3 -c 'for i in range(0,int(512/4)): print("abc\n", end="")')"
|
||||||
|
local quota_exceeded="no"
|
||||||
|
|
||||||
|
while ! have_test_failures && [ $msgidx -lt 10 ]; do
|
||||||
|
record ""
|
||||||
|
record "[send msg $msgidx]"
|
||||||
|
local subj="msg$msgidx - $(generate_password)"
|
||||||
|
output="$($PYMAIL -smtp-debug -body-from-stdin -no-delete -subj "$subj" $PRIVATE_IP $alice alice <<<"$body" 2>&1)"
|
||||||
|
if ! assert_python_success $? "$output"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
# You'd expect that the send would fail when the quota is
|
||||||
|
# exceeded, but it doesn't. Postfix accepts it into it's
|
||||||
|
# queue, then bounces the message back to sender with
|
||||||
|
# delivery status notification (DSN) of 5.2.2 when it
|
||||||
|
# processes the queue.
|
||||||
|
#
|
||||||
|
# The debugging messages (turned on by the -smtp-debug
|
||||||
|
# argument) hold the internal postfix message id, so
|
||||||
|
# extract that, then grep the logs to see if the message
|
||||||
|
# was bounced due to 5.2.2.
|
||||||
|
|
||||||
|
local postid="$(awk '/^data: .* queued as/ { match($0," as "); print substr($0,RSTART+4,10); exit }' <<<"$output" 2>>$TEST_OF)"
|
||||||
|
record "Extracted POSTID=$postid"
|
||||||
|
if [ ! -z "$postid" ]; then
|
||||||
|
/usr/sbin/postqueue -f >>"$TEST_OF" 2>&1
|
||||||
|
flush_logs
|
||||||
|
record "[dovecot and postfix logs for msg $msgidx]"
|
||||||
|
record "logs: $(grep "$postid" /var/log/mail.log)"
|
||||||
|
|
||||||
|
if grep "$postid" /var/log/mail.log | grep "status=bounced" | grep -Fq "5.2.2"; then
|
||||||
|
# success - message was rejected
|
||||||
|
quota_exceeded="yes"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
subjects+=( "$subj" )
|
||||||
|
let msgidx+=1
|
||||||
|
# doveadm quota get -u "$alice" >>"$TEST_OF" 2>&1
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! have_test_failures && [ "$quota_exceeded" = "no" ]; then
|
||||||
|
test_failure "Quota restriction was not enforced by dovecot after sending $msgidx messages"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# cleanup: delete the messages
|
||||||
|
msgidx=0
|
||||||
|
for subj in "${subjects[@]}"; do
|
||||||
|
record "[delete msg $msgidx]"
|
||||||
|
record "subj=$subj"
|
||||||
|
$PYMAIL -no-send -timeout 2 -subj "$subj" $PRIVATE_IP $alice alice >>$TEST_OF 2>&1
|
||||||
|
let msgidx+=1
|
||||||
|
done
|
||||||
|
|
||||||
|
# verify cleanup worked
|
||||||
|
local cur_count_messages="$(doveadm -f json quota get -u "$alice" | jq -r '.[] | select(.type=="MESSAGE") | .value')"
|
||||||
|
if [ $count_messages -ne $cur_count_messages ]; then
|
||||||
|
test_failure "Cleanup failed: test account $alice started with $count_messages but ended up with $cur_count_messages"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# cleanup: delete the test user
|
||||||
|
delete_user "$alice"
|
||||||
|
test_end
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
suite_start "management-users" mgmt_start
|
suite_start "management-users" mgmt_start
|
||||||
|
|
||||||
test_totp
|
test_totp
|
||||||
test_mixed_case_domains
|
test_mixed_case_domains
|
||||||
test_mixed_case_users
|
test_mixed_case_users
|
||||||
test_intl_domains
|
test_intl_domains
|
||||||
|
test_mailbox_quotas
|
||||||
|
|
||||||
suite_end mgmt_end
|
suite_end mgmt_end
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ export UPSTREAM_TAG="${UPSTREAM_TAG:-}"
|
|||||||
|
|
||||||
# For setup scripts that install miabldap releases (eg. upgrade tests)
|
# For setup scripts that install miabldap releases (eg. upgrade tests)
|
||||||
export MIABLDAP_GIT="${MIABLDAP_GIT:-https://github.com/downtownallday/mailinabox-ldap.git}"
|
export MIABLDAP_GIT="${MIABLDAP_GIT:-https://github.com/downtownallday/mailinabox-ldap.git}"
|
||||||
export MIABLDAP_RELEASE_TAG="${MIABLDAP_RELEASE_TAG:-v60}"
|
export MIABLDAP_RELEASE_TAG="${MIABLDAP_RELEASE_TAG:-v70}"
|
||||||
|
|
||||||
# When running tests that require php, use this version of php. This
|
# When running tests that require php, use this version of php. This
|
||||||
# should be the same as what's in setup/functions.sh.
|
# should be the same as what's in setup/functions.sh.
|
||||||
@ -73,3 +73,4 @@ export PHP_VER=8.0
|
|||||||
# Tag of last version supported on Ubuntu Bionic 18.04
|
# Tag of last version supported on Ubuntu Bionic 18.04
|
||||||
UPSTREAM_FINAL_RELEASE_TAG_BIONIC64=v57a
|
UPSTREAM_FINAL_RELEASE_TAG_BIONIC64=v57a
|
||||||
MIABLDAP_FINAL_RELEASE_TAG_BIONIC64=f6cd8f56c3bcb20969c6cf66c040c8efa3773f3a
|
MIABLDAP_FINAL_RELEASE_TAG_BIONIC64=f6cd8f56c3bcb20969c6cf66c040c8efa3773f3a
|
||||||
|
MIABLDAP_INITIAL_RELEASE_TAG_JAMMY64=v60
|
||||||
|
@ -28,6 +28,8 @@ def usage():
|
|||||||
print(" -no-send: don't send, just delete")
|
print(" -no-send: don't send, just delete")
|
||||||
print(" -no-delete: don't delete, just send")
|
print(" -no-delete: don't delete, just send")
|
||||||
print(" -timeout <seconds>: how long to wait for message")
|
print(" -timeout <seconds>: how long to wait for message")
|
||||||
|
print(" -body-from-stdin: read the message body from stdin")
|
||||||
|
print(" -smtp-debug: output debugging messages")
|
||||||
print("");
|
print("");
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@ -48,6 +50,8 @@ delete_msg=True # login to imap and delete message
|
|||||||
wait_timeout=30 # abandon timeout wiating for message delivery
|
wait_timeout=30 # abandon timeout wiating for message delivery
|
||||||
wait_cycle_sleep=5 # delay between delivery checks
|
wait_cycle_sleep=5 # delay between delivery checks
|
||||||
subject="Mail-in-a-Box Automated Test Message " + uuid.uuid4().hex # message subject
|
subject="Mail-in-a-Box Automated Test Message " + uuid.uuid4().hex # message subject
|
||||||
|
body_from_stdin=False
|
||||||
|
smtp_debug=False
|
||||||
|
|
||||||
# process command line
|
# process command line
|
||||||
argi=1
|
argi=1
|
||||||
@ -81,6 +85,12 @@ while argi<len(sys.argv):
|
|||||||
elif arg=="-timeout" and arg_remaining>1:
|
elif arg=="-timeout" and arg_remaining>1:
|
||||||
wait_timeout=int(sys.argv[argi+1])
|
wait_timeout=int(sys.argv[argi+1])
|
||||||
argi+=2
|
argi+=2
|
||||||
|
elif arg=="-body-from-stdin":
|
||||||
|
body_from_stdin = True
|
||||||
|
argi+=1
|
||||||
|
elif arg=="-smtp-debug":
|
||||||
|
smtp_debug = True
|
||||||
|
argi+=1
|
||||||
else:
|
else:
|
||||||
usage()
|
usage()
|
||||||
|
|
||||||
@ -100,14 +110,20 @@ headerfrom = if_unset(headerfrom, emailfrom)
|
|||||||
emailto = if_unset(emailto, login)
|
emailto = if_unset(emailto, login)
|
||||||
emailto_pw = if_unset(emailto_pw, pw)
|
emailto_pw = if_unset(emailto_pw, pw)
|
||||||
|
|
||||||
|
if body_from_stdin:
|
||||||
|
body=sys.stdin.readlines()
|
||||||
|
else:
|
||||||
|
body=['This is a test message. It should be automatically deleted by the test script.']
|
||||||
|
|
||||||
msg = """From: {headerfrom}
|
msg = """From: {headerfrom}
|
||||||
To: {emailto}
|
To: {emailto}
|
||||||
Subject: {subject}
|
Subject: {subject}
|
||||||
|
|
||||||
This is a test message. It should be automatically deleted by the test script.""".format(
|
{body}""".format(
|
||||||
headerfrom=headerfrom,
|
headerfrom=headerfrom,
|
||||||
emailto=emailto,
|
emailto=emailto,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
|
body=''.join(body)
|
||||||
)
|
)
|
||||||
|
|
||||||
def imap_login(host, login, pw):
|
def imap_login(host, login, pw):
|
||||||
@ -162,7 +178,8 @@ def smtp_login(host, login, pw, port):
|
|||||||
server.starttls()
|
server.starttls()
|
||||||
else:
|
else:
|
||||||
server = smtplib.SMTP_SSL(host)
|
server = smtplib.SMTP_SSL(host)
|
||||||
#server.set_debuglevel(1)
|
if smtp_debug:
|
||||||
|
server.set_debuglevel(1)
|
||||||
|
|
||||||
# Verify that the EHLO name matches the server's reverse DNS.
|
# Verify that the EHLO name matches the server's reverse DNS.
|
||||||
ipaddr = socket.gethostbyname(host) # IPv4 only!
|
ipaddr = socket.gethostbyname(host) # IPv4 only!
|
||||||
@ -191,7 +208,10 @@ def smtp_login(host, login, pw, port):
|
|||||||
if send_msg:
|
if send_msg:
|
||||||
# Attempt to send a mail.
|
# Attempt to send a mail.
|
||||||
server = smtp_login(host, login, pw, port)
|
server = smtp_login(host, login, pw, port)
|
||||||
server.sendmail(emailfrom, [emailto], msg)
|
# sendmail: "If this method does not raise an exception, it returns a dictionary, with one entry for each recipient that was refused. Each entry contains a tuple of the SMTP error code and the accompanying error message sent by the server."
|
||||||
|
errors = server.sendmail(emailfrom, [emailto], msg)
|
||||||
|
#print(errors)
|
||||||
|
#if errors: raise ValueError(errors)
|
||||||
server.quit()
|
server.quit()
|
||||||
print("SMTP submission is OK.")
|
print("SMTP submission is OK.")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user