1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-05 15:57:23 +01:00
Upstream is adding handling for utf8 domains by creating a domain alias @utf8 -> @idna. I'm deviating from this approach by setting multiple email address (idna and utf8) per user and alias where a domain contains non-ascii characters. The maildrop (mailbox) remains the same - all mail goes to the user's mailbox regardless of which email address was used. This is more in line with how other systems (eg. active directory), handle multiple email addresses for a single user.

# Conflicts:
#	README.md
#	management/mailconfig.py
#	management/templates/index.html
#	setup/dns.sh
#	setup/mail-users.sh
This commit is contained in:
downtownallday
2021-10-01 17:43:48 -04:00
30 changed files with 1326 additions and 458 deletions

View File

@@ -80,7 +80,13 @@ remote-control:
EOF
fi
echo "include: /etc/nsd/zones.conf" >> /etc/nsd/nsd.conf;
# Create a directory for additional configuration directives, including
# the zones.conf file written out by our management daemon.
echo "include: /etc/nsd/nsd.conf.d/*.conf" >> /etc/nsd/nsd.conf;
# Remove the old location of zones.conf that we generate. It will
# now be stored in /etc/nsd/nsd.conf.d.
rm -f /etc/nsd/zones.conf
# Create DNSSEC signing keys.

View File

@@ -13,8 +13,13 @@ get_attribute_from_ldif() {
local line
while read line; do
[ -z "$line" ] && break
local v=$(awk "/^$attr:/ { print substr(\$0, length(\"$attr\")+3) }" <<<$line)
[ ! -z "$v" ] && ATTR_VALUE+=( "$v" )
local v=$(awk "/^$attr: / { print substr(\$0, length(\"$attr\")+3) }" <<<"$line")
if [ ! -z "$v" ]; then
ATTR_VALUE+=( "$v" )
else
v=$(awk "/^$attr:: / { print substr(\$0, length(\"$attr\")+4) }" <<<"$line")
[ ! -z "$v" ] && ATTR_VALUE+=( $(base64 --decode --wrap=0 <<<"$v") )
fi
done <<< "$ldif"
return 0
}

View File

@@ -295,8 +295,8 @@ schema_to_ldif() {
local cn="$3" # schema common name, eg "postfix"
local cat='cat'
if [ ! -e "$schema" ]; then
if [ -e "conf/$(basename $schema)" ]; then
schema="conf/$(basename $schema)"
if [ -e "conf/schema/$(basename $schema)" ]; then
schema="conf/schema/$(basename $schema)"
else
cat="curl -s"
fi
@@ -316,56 +316,57 @@ EOF
| sed 's/^\s*$/#/g' >> "$ldif"
}
change_core_mail_syntax() {
# output the ldif to change mail to utf8 from ia5 in the core schema
get_attribute "cn=schema,cn=config" "(cn={0}core)" "olcAttributeTypes"
case "${ATTR_VALUE[48]}" in
*\'mail\'*caseIgnoreIA5Match* )
newval=$(sed 's/SYNTAX[ \t][^ ]*/SYNTAX 1.3.6.1.4.1.1466.115.121.1.15/g' <<<"${ATTR_VALUE[48]}" |
sed 's/caseIgnoreIA5Match/caseIgnoreMatch/g' |
sed 's/caseIgnoreIA5SubstringsMatch/caseIgnoreSubstringsMatch/g')
cat <<EOF
dn: cn={0}core,cn=schema,cn=config
changetype: modify
delete: olcAttributeTypes
olcAttributeTypes: ${ATTR_VALUE[48]}
-
add: olcAttributeTypes
olcAttributeTypes: $newval
EOF
;;
esac
}
add_schemas() {
# Add necessary schema's for MiaB operaion
# Add necessary schema's for MiaB operaion
#
# First, apply rfc822MailMember from OpenLDAP's "misc"
# schema. Don't apply the whole schema file because much is from
# expired RFC's, and we just need rfc822MailMember
local cn="misc"
get_attribute "cn=schema,cn=config" "(&(cn={*}$cn)(objectClass=olcSchemaConfig))" "cn"
if [ -z "$ATTR_DN" ]; then
say_verbose "Adding '$cn' schema"
cat "/etc/ldap/schema/misc.ldif" | awk 'BEGIN {C=0}
/^(dn|objectClass|cn):/ { print $0; next }
/^olcAttributeTypes:/ && /27\.2\.1\.15/ { print $0; C=1; next }
/^(olcAttributeTypes|olcObjectClasses):/ { C=0; next }
/^ / && C==1 { print $0 }' | ldapadd -Q -Y EXTERNAL -H ldapi:/// >/dev/null
fi
# Next, apply the postfix schema from the ldapadmin project
# (GPL)(*).
# Note: the postfix schema originally came from the ldapadmin
# project (GPL)(*), but has been modified to support the needs of
# this project.
# see: http://ldapadmin.org
# http://ldapadmin.org/docs/postfix.schema
# http://www.postfix.org/LDAP_README.html
# (*) mailGroup modified to include rfc822MailMember
local schema="http://ldapadmin.org/docs/postfix.schema"
local cn="postfix"
get_attribute "cn=schema,cn=config" "(&(cn={*}$cn)(objectClass=olcSchemaConfig))" "cn"
if [ -z "$ATTR_DN" ]; then
local ldif="/tmp/$cn.$$.ldif"
schema_to_ldif "$schema" "$ldif" "$cn"
sed -i 's/\$ member \$/$ member $ rfc822MailMember $/' "$ldif"
say_verbose "Adding '$cn' schema"
[ ${verbose:-0} -gt 1 ] && cat "$ldif"
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
rm -f "$ldif"
fi
# apply the mfa-totp schema
# this adds the totpUser class to store the totp secret
local schema="mfa-totp.schema"
local cn="mfa-totp"
get_attribute "cn=schema,cn=config" "(&(cn={*}$cn)(objectClass=olcSchemaConfig))" "cn"
if [ -z "$ATTR_DN" ]; then
local ldif="/tmp/$cn.$$.ldif"
schema_to_ldif "$schema" "$ldif" "$cn"
say_verbose "Adding '$cn' schema"
[ ${verbose:-0} -gt 1 ] && cat "$ldif"
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
rm -f "$ldif"
fi
for cn in "postfix" "mfa-totp" "namedProperties"; do
schema="$cn.schema"
get_attribute "cn=schema,cn=config" "(&(cn={*}$cn)(objectClass=olcSchemaConfig))" "cn"
if [ -z "$ATTR_DN" ]; then
local ldif="/tmp/$cn.$$.ldif"
schema_to_ldif "$schema" "$ldif" "$cn"
if [ "$cn" = "postfix" ]; then
local ldif2="/tmp/$cn.$$-2.ldif"
change_core_mail_syntax >"$ldif2"
echo "" >>"$ldif2"
sed 's/^dn: cn=postfix,\(.*\)$/dn: cn=postfix,\1\nchangetype: add/g' "$ldif" >> "$ldif2"
rm "$ldif"
ldif="$ldif2"
fi
say_verbose "Adding '$cn' schema"
[ ${verbose:-0} -gt 1 ] && cat "$ldif"
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
rm -f "$ldif"
fi
done
}
@@ -504,7 +505,7 @@ add_indexes() {
# Add the indexes
get_attribute "$cdn" "(objectClass=*)" "olcDbIndex" base
local attr
for attr in mail maildrop mailaccess dc rfc822MailMember; do
for attr in mail maildrop mailaccess dc dcIntl mailMember; do
local type="eq" atype="" aindex=""
[ "$attr" == "mail" ] && type="eq,sub"
@@ -661,6 +662,7 @@ process_cmdline() {
elif [ "$1" == "-config" ]; then
# Apply a certain configuration
if [ "$2" == "server" ]; then
add_schemas
modify_global_config
add_overlays
add_indexes
@@ -675,8 +677,23 @@ process_cmdline() {
elif [ "$1" == "-search" ]; then
# search for email addresses, distinguished names and general
# ldap filters
debug_search "$2"
# ldap filters. Args:
# search filter [optional]
# base dn [optional]
# remaining args are attributes to output [optional]
shift
if [ $# -eq 0 ]; then
debug_search "(objectclass=*)"
else
debug_search "$@"
fi
exit 0
elif [ "$1" == "-schema-to-ldif" ]; then
cn="$2"
output="${3:-/dev/stdout}"
schema="$cn.schema"
schema_to_ldif "$schema" "$output" "$cn"
exit 0
elif [ "$1" == "-dumpdb" ]; then
@@ -699,6 +716,11 @@ process_cmdline() {
echo ""
slapcat ${slapcat_args[@]} -s "$ATTR_DN" | grep -Ev "^$hide_attrs:"
fi
if [ "$s" == "all" -o "$s" == "schema" ]; then
echo ""
echo '--------------------------------'
slapcat ${slapcat_args[@]} -s "cn=schema,cn=config" | grep -Ev "^$hide_attrs:"
fi
if [ "$s" == "all" -o "$s" == "frontend" ]; then
echo ""
echo '--------------------------------'
@@ -716,7 +738,7 @@ process_cmdline() {
if [ "$s" == "aliases" ]; then
echo ""
echo '--------------------------------'
local attrs=(mail member mailRoutingAddress rfc822MailMember)
local attrs=(mail member mailRoutingAddress mailMember namedProperty)
[ ${verbose:-0} -gt 0 ] && attrs=()
debug_search "(objectClass=mailGroup)" "$LDAP_ALIASES_BASE" ${attrs[@]}
fi

View File

@@ -63,7 +63,7 @@ base = ${LDAP_USERS_BASE}
# filter below. If found, the user is authenticated against this dn
# (a bind is attempted as that user). The attribute 'mail' is
# multi-valued and contains all the user's email addresses. We use
# maildrop as the dovecot mailbox address and forbid then from using
# maildrop as the dovecot mailbox address and forbid them from using
# it for authentication by excluding maildrop from the filter.
pass_filter = (&(objectClass=mailUser)(mail=%u))
pass_attrs = maildrop=user
@@ -166,6 +166,7 @@ chmod 0640 /etc/postfix/sender-login-maps-aliases.cf
# Check whether a destination email address exists, and to perform any
# email alias rewrites in Postfix.
tools/editconf.py /etc/postfix/main.cf \
smtputf8_enable=no \
virtual_mailbox_domains=ldap:/etc/postfix/virtual-mailbox-domains.cf \
virtual_mailbox_maps=ldap:/etc/postfix/virtual-mailbox-maps.cf \
virtual_alias_maps=ldap:/etc/postfix/virtual-alias-maps.cf \
@@ -180,7 +181,7 @@ bind_dn = ${LDAP_POSTFIX_DN}
bind_pw = ${LDAP_POSTFIX_PASSWORD}
version = 3
search_base = ${LDAP_DOMAINS_BASE}
query_filter = (&(dc=%s)(businessCategory=mail))
query_filter = (&(|(dc=%s)(dcIntl=%s))(businessCategory=mail))
result_attribute = dc
EOF
chgrp postfix /etc/postfix/virtual-mailbox-domains.cf
@@ -228,7 +229,6 @@ chmod 0640 /etc/postfix/virtual-mailbox-maps.cf
# it might have just permitted_senders, skip any records with an
# empty destination here so that other lower priority rules might match.
#
# This is the ldap version of aliases(5) but for virtual
# addresses. Postfix queries this recursively to determine delivery
@@ -242,7 +242,7 @@ bind_pw = ${LDAP_POSTFIX_PASSWORD}
version = 3
search_base = ${LDAP_USERS_BASE}
query_filter = (mail=%s)
result_attribute = maildrop, rfc822MailMember
result_attribute = maildrop, mailMember
special_result_attribute = member
EOF
chgrp postfix /etc/postfix/virtual-alias-maps.cf

View File

@@ -187,6 +187,11 @@ def migration_13(env):
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);"])
def migration_14(env):
# Add the "auto_aliases" table.
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);"])
###########################################################
@@ -245,9 +250,77 @@ def migration_miabldap_1(env):
aliases=m13.create_aliases(env, conn, ldap, ldap_aliases_base)
permitted=m13.create_permitted_senders(conn, ldap, ldap_users_base, ldap_permitted_senders_base)
m13.populate_aliases(conn, ldap, users, aliases)
ldap.unbind()
conn.close()
def migration_miabldap_2(env):
# This migration step changes the ldap schema to support utf8 email
#
# possible states at this point:
# miabldap was installed and is being upgraded
# -> old pre-utf8 schema present
# a miab install was present and step 1 upgaded it to miabldap
# -> new utf8 schema 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)
ldap_domains_base = ldapvars.LDAP_DOMAINS_BASE
ldap_aliases_base = ldapvars.LDAP_ALIASES_BASE
ldap_users_base = ldapvars.LDAP_USERS_BASE
# 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",
"-LLL",
"olcObjectClasses"
])
if "rfc822MailMember" in ret:
def ldif_change_fn(ldif):
return ldif.replace("rfc822MailMember: ", "mailMember: ")
# apply schema changes miabldap/1 -> miabldap/2
ldap.unbind()
print("Apply schema changes")
m14.apply_schema_changes(env, ldapvars, ldif_change_fn)
# reconnect
ldap = connect(ldapvars)
# 3. migrate to utf8: users, aliases and domains
print("Create utf8 entries for users and aliases having IDNA domains")
m14.add_utf8_mail_addresses(env, ldap, ldap_users_base)
print("Add namedProperties objectclass to aliases")
m14.add_namedProperties_objectclass(env, ldap, ldap_aliases_base)
print("Add mailDomain objectclass to domains")
m14.add_mailDomain_objectclass(env, ldap, ldap_domains_base)
print("Mark required aliases with 'auto' property")
m14.add_auto_tag(env, ldap, ldap_aliases_base)
print("Ensure all required aliases are created")
m14.ensure_required_aliases(env, ldapvars, ldap)
ldap.unbind()
def get_current_migration():
ver = 0
@@ -327,11 +400,20 @@ def run_miabldap_migrations():
env = load_environment()
migration_id_file = os.path.join(env['STORAGE_ROOT'], 'mailinabox-ldap.version')
migration_id = 0
migration_id = None
if os.path.exists(migration_id_file):
with open(migration_id_file) as f:
migration_id = f.read().strip();
if migration_id.strip()=='': migration_id = None
if migration_id is None:
if os.path.exists(os.path.join(env['STORAGE_ROOT'], 'mailinabox.version')):
migration_id = 0
else:
print()
print("%s file doesn't exists. Skipping migration..." % (migration_id_file,))
return
ourver = int(migration_id)
while True:

232
setup/migration_14.py Normal file
View File

@@ -0,0 +1,232 @@
#!/usr/bin/python3
# -*- indent-tabs-mode: t; tab-width: 4; python-indent-offset: 4; -*-
#
# helper functions for migration #14 / miabldap-migration #2
#
import sys, os, ldap3, idna
from utils import shell
from mailconfig import (
add_required_aliases,
required_alias_names,
get_mail_domains
)
def utf8_from_idna(domain_idna):
try:
return idna.decode(domain_idna.encode("ascii"))
except (UnicodeError, idna.IDNAError):
# Failed to decode IDNA, should never happen
return domain_idna
def apply_schema_changes(env, ldapvars, ldif_change_fn):
# 1. save LDAP_BASE data to ldif
slapd_conf = os.path.join(env["STORAGE_ROOT"], "ldap/slapd.d")
fail_fn = os.path.join(env["STORAGE_ROOT"], "ldap/failed_migration.txt")
ldif = shell("check_output", [
"/usr/sbin/slapcat",
"-F", slapd_conf,
"-b", ldapvars.LDAP_BASE
])
# 2. wipe out existing database configuration and database
# 2a. set the creation parameters
ORGANIZATION="Mail-In-A-Box"
LDAP_DOMAIN="mailinabox"
shell("check_output", [
"/usr/bin/debconf-set-selections"
], input=f'''slapd shared/organization string {ORGANIZATION}
slapd slapd/domain string {LDAP_DOMAIN}
slapd slapd/password1 password {ldapvars.LDAP_ADMIN_PASSWORD}
slapd slapd/password2 password {ldapvars.LDAP_ADMIN_PASSWORD}
'''.encode('utf-8')
)
# 2b. recreate ldap config and database
shell("check_call", [
"/usr/sbin/dpkg-reconfigure",
"--frontend=noninteractive",
"slapd"
])
# 2c. clear passwords from debconf
shell("check_output", [
"/usr/bin/debconf-set-selections"
], input='''slapd slapd/password1 password
slapd slapd/password2 password
'''.encode('utf-8')
)
# 3. make desired ldif changes
# 3a. first, remove dc=mailinabox and
# cn=admin,dc=mailinabox. they were both created during
# dpkg-reconfigure and can't be readded
entries = ldif.split("\n\n")
keep = []
removed = []
remove = [
"dn: " + ldapvars.LDAP_BASE,
"dn: " + ldapvars.LDAP_ADMIN_DN
]
for entry in entries:
dn = entry.split("\n")[0]
if dn not in remove:
keep.append(entry)
else:
removed.append(entry)
# 3b. call the given ldif change function
ldif = ldif_change_fn("\n\n".join(keep))
#ldif = ldif_change_fn(ldif)
# 4. re-create schemas and other config
shell("check_call", [
"setup/ldap.sh",
"-v",
"-config", "server"
])
# 5. restore LDAP_BASE data
code, ret = shell("check_output", [
"/usr/sbin/slapadd",
"-F", slapd_conf,
"-b", ldapvars.LDAP_BASE,
"-v",
"-c"
], input=ldif.encode('utf-8'), trap=True, capture_stderr=True)
if code != 0:
try:
with open(fail_fn, "w") as of:
of.write("# slapadd -F %s -b %s -v -c\n" %
(slapd_conf, ldapvars.LDAP_BASE))
of.write(ldif)
print("See saved data in %s" % fail_fn)
except Exception:
pass
raise ValueError("Could not restore data: exit code=%s: output=%s" % (code, ret))
def add_utf8_mail_addresses(env, ldap, ldap_users_base):
# if the mail attribute of users or aliases is idna encoded, also
# add a utf8 version of the address to the mail attribute so the
# user or alias will be known by multiple addresses (idna and
# utf8)
pager = ldap.paged_search(ldap_users_base, "(|(objectClass=mailGroup)(objectClass=mailUser))", attributes=['mail'])
changes = []
for rec in pager:
mail_idna_lc = []
for addr in rec['mail']:
mail_idna_lc = addr.lower()
changed = False
new_mail = []
for addr in rec['mail']:
new_mail.append(addr)
name = addr.split('@')[0]
domain = addr.split('@', 1)[1]
addr_utf8 = name + '@' + utf8_from_idna(domain)
addr_utf8_lc = addr_utf8.lower()
if addr_utf8 != addr and addr_utf8_lc not in mail_lc:
new_mail.append(addr_utf8)
print("Add '%s' for %s" % (addr_utf8, addr))
changed = True
if changed:
changes.append({"rec":rec, "mail":new_mail})
for change in changes:
ldap.modify_record(
change["rec"],
{ "mail": change["mail"] }
)
def add_namedProperties_objectclass(env, ldap, ldap_aliases_base):
# ensure every alias has a namedProperties objectClass attached
pager = ldap.paged_search(ldap_aliases_base, "(&(objectClass=mailGroup)(!(objectClass=namedProperties)))", attributes=['objectClass'])
changes = []
for rec in pager:
newoc = rec['objectClass'].copy()
newoc.append('namedProperties')
changelist = {
'objectClass': newoc,
}
changes.append({'rec': rec, 'changelist': changelist})
for change in changes:
ldap.modify_record(change['rec'], change['changelist'])
def add_auto_tag(env, ldap, ldap_aliases_base):
# add namedProperty=auto to existing required aliases
# this step is needed to upgrade miabldap systems
name_q = [
"(mail=hostmaster@"+env['PRIMARY_HOSTNAME']+")"
]
for name in required_alias_names:
name_q.append("(mail=%s@*)" % name)
q = [
"(objectClass=mailGroup)",
"(!(namedProperty=auto))",
"(|%s)" % "".join(name_q)
]
pager = ldap.paged_search(
ldap_aliases_base,
"(&%s)" % "".join(q),
attributes=['namedProperty']
)
changes = []
for rec in pager:
newval = rec["namedProperty"].copy()
newval.append("auto")
changes.append({"rec": rec, "namedProperty": newval})
for change in changes:
ldap.modify_record(
change["rec"],
{"namedProperty": change["namedProperty"]}
)
def add_mailDomain_objectclass(env, ldap, ldap_domains_base):
# ensure every domain has a mailDomain objectClass attached
pager = ldap.paged_search(ldap_domains_base, "(&(objectClass=domain)(!(objectClass=mailDomain)))", attributes=['objectClass', 'dc', 'dcIntl'])
changes = []
for rec in pager:
newoc = rec['objectClass'].copy()
newoc.append('mailDomain')
changelist = {
'objectClass': newoc,
'dcIntl': [ utf8_from_idna(rec['dc'][0]) ]
}
changes.append({'rec': rec, 'changelist': changelist})
for change in changes:
ldap.modify_record(change['rec'], change['changelist'])
def ensure_required_aliases(env, ldapvars, ldap):
# ensure every domain has its required aliases
env_combined = env.copy()
env_combined.update(ldapvars)
errors = []
for domain_idna in get_mail_domains(ldapvars):
results = add_required_aliases(env_combined, ldap, domain_idna)
for result in results:
if isinstance(result, str):
print(result)
else:
print("Error: %s" % result[0])
errors.append(result[0])
if len(errors)>0:
raise ValueError("Some required aliases could not be added")