mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-01 23:57:05 +00:00
# Conflicts: # management/daemon.py # management/mailconfig.py # management/templates/users.html # setup/bootstrap.sh # setup/mail-postfix.sh # setup/mail-users.sh # setup/migrate.py
278 lines
8.5 KiB
Python
278 lines
8.5 KiB
Python
#!/usr/bin/python3
|
|
# -*- indent-tabs-mode: t; tab-width: 4; python-indent-offset: 4; -*-
|
|
#####
|
|
##### This file is part of Mail-in-a-Box-LDAP which is released under the
|
|
##### terms of the GNU Affero General Public License as published by the
|
|
##### Free Software Foundation, either version 3 of the License, or (at
|
|
##### your option) any later version. See file LICENSE or go to
|
|
##### https://github.com/downtownallday/mailinabox-ldap for full license
|
|
##### details.
|
|
#####
|
|
|
|
|
|
#
|
|
# helper functions for migration #13
|
|
#
|
|
|
|
import uuid, os, sqlite3, ldap3, hashlib
|
|
|
|
|
|
def add_user(env, ldapconn, search_base, users_base, domains_base, email, password, privs, quota, totp, cn=None):
|
|
# Add a sqlite user to ldap
|
|
# env are the environment variables
|
|
# ldapconn is the bound ldap connection
|
|
# search_base is for finding a user with the same email
|
|
# users_base is the rdn where the user will be added
|
|
# domains_base is the rdn for 'domain' entries
|
|
# email is the user's email
|
|
# password is the user's current sqlite password hash
|
|
# 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
|
|
# cn is the user's common name [optional]
|
|
#
|
|
# the email address should be as-is from sqlite (encoded as
|
|
# ascii using IDNA rules)
|
|
|
|
# If the email address exists, return and do nothing
|
|
ldapconn.search(search_base, "(mail=%s)" % email)
|
|
if len(ldapconn.entries) > 0:
|
|
print("user already exists: %s" % email)
|
|
return ldapconn.response[0]['dn']
|
|
|
|
## Generate a unique id for uid
|
|
#uid = '%s' % uuid.uuid4()
|
|
# use a sha-1 hash of the email address for uid
|
|
m = hashlib.sha1()
|
|
m.update(bytearray(email.lower(),'utf-8'))
|
|
uid = m.hexdigest()
|
|
|
|
# Attributes to apply to the new ldap entry
|
|
objectClasses = [ 'inetOrgPerson','mailUser','shadowAccount' ]
|
|
attrs = {
|
|
"mail" : email,
|
|
"maildrop" : email,
|
|
"uid" : uid,
|
|
"mailboxQuota": quota,
|
|
# Openldap uses prefix {CRYPT} for all crypt(3) formats
|
|
"userPassword" : password.replace('{SHA512-CRYPT}','{CRYPT}')
|
|
}
|
|
|
|
# Add privileges ('mailaccess' attribute)
|
|
privs_uniq = {}
|
|
for priv in privs:
|
|
if priv.strip() != '': privs_uniq[priv] = True
|
|
if len(privs_uniq) > 0:
|
|
attrs['mailaccess'] = list(privs_uniq.keys())
|
|
|
|
# Get a common name
|
|
localpart, domainpart = email.split("@")
|
|
|
|
if cn is None:
|
|
# Get the name for the email address from Roundcube and
|
|
# use that or `localpart` if no name
|
|
rconn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite"))
|
|
rc = rconn.cursor()
|
|
rc.execute("SELECT name FROM identities WHERE email = ? AND standard = 1 AND del = 0 AND name <> ''", (email,))
|
|
rc_all = rc.fetchall()
|
|
if len(rc_all)>0:
|
|
cn = rc_all[0][0]
|
|
attrs["displayName"] = cn
|
|
else:
|
|
cn = localpart.replace('.',' ').replace('_',' ')
|
|
rconn.close()
|
|
attrs["cn"] = cn
|
|
|
|
# Choose a surname for the user (required attribute)
|
|
attrs["sn"] = cn[cn.find(' ')+1:]
|
|
|
|
# add TOTP, if enabled
|
|
if totp:
|
|
objectClasses.append('totpUser')
|
|
attrs['totpSecret'] = totp["secret"]
|
|
attrs['totpMruToken'] = totp["mru_token"]
|
|
attrs['totpMruTokenTime'] = totp["mru_token_time"]
|
|
attrs['totpLabel'] = totp["label"]
|
|
|
|
# Add user
|
|
dn = "uid=%s,%s" % (uid, users_base)
|
|
|
|
print("adding user %s" % email)
|
|
ldapconn.add(dn, objectClasses, attrs)
|
|
|
|
# Create domain entry indicating that we are handling
|
|
# mail for that domain
|
|
domain_dn = 'dc=%s,%s' % (domainpart, domains_base)
|
|
try:
|
|
ldapconn.add(domain_dn, [ 'domain' ], {
|
|
"businessCategory": "mail"
|
|
})
|
|
except ldap3.core.exceptions.LDAPEntryAlreadyExistsResult:
|
|
pass
|
|
return dn
|
|
|
|
|
|
def create_users(env, conn, ldapconn, ldap_base, ldap_users_base, ldap_domains_base):
|
|
# iterate through sqlite 'users' table and create each user in
|
|
# ldap. returns a map of email->dn
|
|
|
|
# select users
|
|
c = conn.cursor()
|
|
c.execute("SELECT id, email, password, privileges, quota from users")
|
|
|
|
users = {}
|
|
for row in c:
|
|
user_id=row[0]
|
|
email=row[1]
|
|
password=row[2]
|
|
privs=row[3]
|
|
quota=row[4]
|
|
totp = None
|
|
|
|
c2 = conn.cursor()
|
|
c2.execute("SELECT secret, mru_token, label from mfa where user_id=? and type='totp'", (user_id,));
|
|
rowidx = 0
|
|
for row2 in c2:
|
|
if totp is None:
|
|
totp = {
|
|
"secret": [],
|
|
"mru_token": [],
|
|
"mru_token_time": [],
|
|
"label": []
|
|
}
|
|
totp["secret"].append("{%s}%s" % (rowidx, row2[0]))
|
|
totp["mru_token"].append("{%s}%s" % (rowidx, row2[1] or ''))
|
|
totp["mru_token_time"].append("{%s}%s" % (rowidx, rowidx))
|
|
totp["label"].append("{%s}%s" % (rowidx, row2[2] or ''))
|
|
rowidx += 1
|
|
|
|
dn = add_user(env, ldapconn, ldap_base, ldap_users_base, ldap_domains_base, email, password, privs.split("\n"), quota, totp)
|
|
users[email] = dn
|
|
return users
|
|
|
|
|
|
def create_aliases(env, conn, ldapconn, aliases_base):
|
|
# iterate through sqlite 'aliases' table and create ldap
|
|
# aliases but without members. returns a map of alias->dn
|
|
aliases={}
|
|
c = conn.cursor()
|
|
for row in c.execute("SELECT source FROM aliases WHERE destination<>''"):
|
|
alias=row[0]
|
|
ldapconn.search(aliases_base, "(mail=%s)" % alias)
|
|
if len(ldapconn.entries) > 0:
|
|
# Already present
|
|
print("alias already exists %s" % alias)
|
|
aliases[alias] = ldapconn.response[0]['dn']
|
|
else:
|
|
cn="%s" % uuid.uuid4()
|
|
dn="cn=%s,%s" % (cn, aliases_base)
|
|
description="Mail group %s" % alias
|
|
|
|
if alias.startswith("postmaster@") or \
|
|
alias.startswith("hostmaster@") or \
|
|
alias.startswith("abuse@") or \
|
|
alias.startswith("admin@") or \
|
|
alias == "administrator@" + env['PRIMARY_HOSTNAME']:
|
|
description = "Required alias"
|
|
|
|
print("adding alias %s" % alias)
|
|
ldapconn.add(dn, ['mailGroup'], {
|
|
"mail": alias,
|
|
"description": description
|
|
})
|
|
aliases[alias] = dn
|
|
return aliases
|
|
|
|
|
|
def populate_aliases(conn, ldapconn, users_map, aliases_map):
|
|
# populate alias with members.
|
|
# conn is a connection to the users sqlite database
|
|
# ldapconn is a connecton to the ldap database
|
|
# users_map is a map of email -> dn for every user on the system
|
|
# aliases_map is a map of email -> dn for every pre-created alias
|
|
#
|
|
# email addresses should be encoded as-is from sqlite (IDNA
|
|
# domains)
|
|
c = conn.cursor()
|
|
for row in c.execute("SELECT source,destination FROM aliases where destination<>''"):
|
|
alias=row[0]
|
|
alias_dn=aliases_map[alias]
|
|
members = []
|
|
mailMembers = []
|
|
|
|
for email in row[1].split(','):
|
|
email=email.strip()
|
|
if email=="":
|
|
continue
|
|
elif email in users_map:
|
|
members.append(users_map[email])
|
|
elif email in aliases_map:
|
|
members.append(aliases_map[email])
|
|
else:
|
|
mailMembers.append(email)
|
|
|
|
print("populate alias group %s" % alias)
|
|
changes = {}
|
|
if len(members)>0:
|
|
changes["member"]=[(ldap3.MODIFY_REPLACE, members)]
|
|
if len(mailMembers)>0:
|
|
changes["rfc822MailMember"]=[(ldap3.MODIFY_REPLACE, mailMembers)]
|
|
ldapconn.modify(alias_dn, changes)
|
|
|
|
|
|
def add_permitted_senders_group(ldapconn, users_base, group_base, source, permitted_senders):
|
|
# creates a single permitted_senders ldap group
|
|
#
|
|
# email addresses should be encoded as-is from sqlite (IDNA
|
|
# domains)
|
|
|
|
# If the group already exists, return and do nothing
|
|
ldapconn.search(group_base, "(&(objectClass=mailGroup)(mail=%s))" % source)
|
|
if len(ldapconn.entries) > 0:
|
|
return ldapconn.response[0]['dn']
|
|
|
|
# get a dn for every permitted sender
|
|
permitted_dn = {}
|
|
for email in permitted_senders:
|
|
email = email.strip()
|
|
if email == "": continue
|
|
ldapconn.search(users_base, "(mail=%s)" % email)
|
|
for result in ldapconn.response:
|
|
permitted_dn[result["dn"]] = True
|
|
if len(permitted_dn) == 0:
|
|
return None
|
|
|
|
# add permitted senders group for the 'source' email
|
|
gid = '%s' % uuid.uuid4()
|
|
group_dn = "cn=%s,%s" % (gid, group_base)
|
|
print("adding permitted senders group for %s" % source)
|
|
try:
|
|
ldapconn.add(group_dn, [ "mailGroup" ], {
|
|
"cn" : gid,
|
|
"mail" : source,
|
|
"member" : list(permitted_dn.keys()),
|
|
"description": "Permitted to MAIL FROM this address"
|
|
})
|
|
except ldap3.core.exceptions.LDAPEntryAlreadyExistsResult:
|
|
pass
|
|
return group_dn
|
|
|
|
|
|
def create_permitted_senders(conn, ldapconn, users_base, group_base):
|
|
# iterate through the 'aliases' table and create all
|
|
# permitted-senders groups
|
|
c = conn.cursor()
|
|
c.execute("SELECT source, permitted_senders from aliases WHERE permitted_senders is not null")
|
|
groups={}
|
|
for row in c:
|
|
source=row[0]
|
|
senders=[]
|
|
for line in row[1].split("\n"):
|
|
for sender in line.split(","):
|
|
if sender.strip() != "":
|
|
senders.append(sender.strip())
|
|
dn=add_permitted_senders_group(ldapconn, users_base, group_base, source, senders)
|
|
if dn is not None:
|
|
groups[source] = dn
|
|
return groups
|