1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-01 23:57:05 +00:00
mailinabox/setup/migration_13.py
downtownallday a6f69f297b Merge remote-tracking branch 'chadfurman/master' into chads-quota
# 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
2024-09-06 12:03:08 -04:00

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