From e4ffe3403a87d8984fb230dfe857584224bf6885 Mon Sep 17 00:00:00 2001
From: nd <git@notandy.de>
Date: Sat, 19 Sep 2020 00:26:19 +0200
Subject: [PATCH] finished mail mapping editing support

---
 uffd/default_config.cfg       |  3 ++
 uffd/ldap/__init__.py         |  2 +-
 uffd/ldap/ldap.py             |  8 +++
 uffd/mail/models.py           | 39 +++------------
 uffd/mail/templates/mail.html | 37 ++++++++++++++
 uffd/mail/views.py            | 92 ++++++++++-------------------------
 6 files changed, 83 insertions(+), 98 deletions(-)
 create mode 100644 uffd/mail/templates/mail.html

diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg
index 1f93e78e..e7e27480 100644
--- a/uffd/default_config.cfg
+++ b/uffd/default_config.cfg
@@ -1,9 +1,11 @@
 LDAP_BASE_USER="ou=users,dc=example,dc=com"
 LDAP_BASE_GROUPS="ou=groups,dc=example,dc=com"
 LDAP_BASE_MAIL="ou=postfix,dc=example,dc=com"
+
 LDAP_SERVICE_BIND_DN=""
 LDAP_SERVICE_BIND_PASSWORD=""
 LDAP_SERVICE_URL="ldapi:///"
+
 LDAP_USER_OBJECTCLASSES=["top", "inetOrgPerson", "organizationalPerson", "person", "posixAccount"]
 LDAP_USER_GID=20001
 LDAP_USER_MIN_UID=10000
@@ -21,6 +23,7 @@ MAIL_USERNAME='yourId@gmail.com'
 MAIL_PASSWORD='*****'
 MAIL_USE_STARTTLS=True
 MAIL_FROM_ADDRESS='foo@bar.com'
+MAIL_LDAP_OBJECTCLASSES=["top", "postfixVirtual"]
 
 ROLES_BASEROLES=['base']
 
diff --git a/uffd/ldap/__init__.py b/uffd/ldap/__init__.py
index 436be193..26171f0d 100644
--- a/uffd/ldap/__init__.py
+++ b/uffd/ldap/__init__.py
@@ -1,6 +1,6 @@
 from .ldap import bp as ldap_bp
 from .ldap import get_conn, user_conn, escape_filter_chars, uid_to_dn
-from .ldap import loginname_to_dn, get_next_uid, loginname_is_safe
+from .ldap import loginname_to_dn, mail_to_dn, get_next_uid, loginname_is_safe, mailname_is_safe
 from .ldap import get_ldap_array_attribute_safe, get_ldap_attribute_safe
 
 bp = [ldap_bp]
diff --git a/uffd/ldap/ldap.py b/uffd/ldap/ldap.py
index 39fee2ff..8380d946 100644
--- a/uffd/ldap/ldap.py
+++ b/uffd/ldap/ldap.py
@@ -44,6 +44,11 @@ def loginname_to_dn(loginname):
 		return 'uid={},{}'.format(loginname, current_app.config["LDAP_BASE_USER"])
 	raise Exception('unsafe login name')
 
+def mail_to_dn(uid):
+	if mailname_is_safe(uid):
+		return 'uid={},{}'.format(uid, current_app.config["LDAP_BASE_MAIL"])
+	raise Exception('unsafe mail name')
+
 def loginname_is_safe(value):
 	if len(value) > 32 or len(value) < 1:
 		return False
@@ -52,6 +57,9 @@ def loginname_is_safe(value):
 			return False
 	return True
 
+def mailname_is_safe(value):
+	return loginname_is_safe(value)
+
 def get_next_uid():
 	conn = get_conn()
 	conn.search(current_app.config["LDAP_BASE_USER"], '(objectclass=person)')
diff --git a/uffd/mail/models.py b/uffd/mail/models.py
index 1e404b1b..dd8d810b 100644
--- a/uffd/mail/models.py
+++ b/uffd/mail/models.py
@@ -32,42 +32,19 @@ class Mail():
 	def to_ldap(self, new=False):
 		conn = ldap.get_conn()
 		if new:
-			self.uid = ldap.get_next_uid()
 			attributes = {
-				'uidNumber': self.uid,
-				'gidNumber': current_app.config['LDAP_USER_GID'],
-				'homeDirectory': '/home/'+self.loginname,
-				'sn': ' ',
-				'userPassword': hashed(HASHED_SALTED_SHA512, secrets.token_hex(128)),
+				'uid': self.uid,
 				# same as for update
-				'givenName': self.displayname,
-				'displayName': self.displayname,
-				'cn': self.displayname,
-				'mail': self.mail,
+				'mailacceptinggeneralid': self.receivers,
+				'maildrop': self.destinations,
 			}
-			dn = ldap.loginname_to_dn(self.loginname)
-			result = conn.add(dn, current_app.config['LDAP_USER_OBJECTCLASSES'], attributes)
+			self.dn = ldap.mail_to_dn(self.uid)
+			result = conn.add(self.dn, current_app.config['MAIL_LDAP_OBJECTCLASSES'], attributes)
 		else:
 			attributes = {
-				'givenName': [(MODIFY_REPLACE, [self.displayname])],
-				'displayName': [(MODIFY_REPLACE, [self.displayname])],
-				'cn': [(MODIFY_REPLACE, [self.displayname])],
-				'mail': [(MODIFY_REPLACE, [self.mail])],
+				'mailacceptinggeneralid': [(MODIFY_REPLACE, self.receivers)],
+				'maildrop': [(MODIFY_REPLACE, self.destinations)],
 				}
-			if self.newpassword:
-				attributes['userPassword'] = [(MODIFY_REPLACE, [hashed(HASHED_SALTED_SHA512, self.newpassword)])]
-			dn = ldap.uid_to_dn(self.uid)
-			result = conn.modify(dn, attributes)
-		self.dn = dn
-
-		group_conn = ldap.get_conn()
-		for group in self.initial_groups_ldap:
-			if not group in self.groups_ldap:
-				group_conn.modify(group, {'uniqueMember': [(MODIFY_DELETE, [self.dn])]})
-		for group in self.groups_ldap:
-			if not group in self.initial_groups_ldap:
-				group_conn.modify(group, {'uniqueMember': [(MODIFY_ADD, [self.dn])]})
-		self.groups_changed = False
-
+			result = conn.modify(self.dn, attributes)
 		return result
 
diff --git a/uffd/mail/templates/mail.html b/uffd/mail/templates/mail.html
new file mode 100644
index 00000000..36021697
--- /dev/null
+++ b/uffd/mail/templates/mail.html
@@ -0,0 +1,37 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<form action="{{ url_for("mail.update", uid=mail.uid) }}" method="POST">
+<div class="align-self-center">
+	<div class="form-group col">
+		<label for="mail-name">Name</label>
+		<input type="text" class="form-control" id="mail-name" name="mail-uid" {% if mail.uid %} value="{{ mail.uid }}" readonly {% else %} value=""{% endif %}>
+		<small class="form-text text-muted">
+		</small>
+	</div>
+	<div class="form-group col">
+		<label for="mail-receivers">Receiving addresses</label>
+		<textarea rows="10" class="form-control" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea>
+		<small class="form-text text-muted">
+			One address per line
+		</small>
+	</div>
+	<div class="form-group col">
+		<label for="mail-destinations">Destinations</label>
+		<textarea rows="10" class="form-control" name="mail-destinations">{{ mail.destinations|join('\n') }}</textarea>
+		<small class="form-text text-muted">
+			One address per line
+		</small>
+	</div>
+	<div class="form-group col">
+		<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
+		<a href="{{ url_for("mail.index") }}" class="btn btn-secondary">Cancel</a>
+		{% if mail.uid %}
+			<a href="{{ url_for("mail.delete", uid=mail.uid) }}" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
+		{% else %}
+			<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
+		{% endif %}
+	</div>
+</div>
+</form>
+{% endblock %}
diff --git a/uffd/mail/views.py b/uffd/mail/views.py
index 460ca53b..8ad6410d 100644
--- a/uffd/mail/views.py
+++ b/uffd/mail/views.py
@@ -32,92 +32,52 @@ def index():
 @bp.route("/<uid>")
 @bp.route("/new")
 def show(uid=None):
-	return None
 	if not uid:
-		user = User()
-		ldif = '<none yet>'
+		mail = Mail()
 	else:
 		conn = get_conn()
-		conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
+		conn.search(current_app.config["LDAP_BASE_MAIL"], '(&(objectclass=postfixVirtual)(uid={}))'.format((escape_filter_chars(uid))))
 		assert len(conn.entries) == 1
-		user = User.from_ldap(conn.entries[0])
-		ldif = conn.entries[0].entry_to_ldif()
-	return render_template('user.html', user=user, user_ldif=ldif, roles=Role.query.all())
+		mail = Mail.from_ldap(conn.entries[0])
+	return render_template('mail.html', mail=mail)
 
 @bp.route("/<uid>/update", methods=['POST'])
 @bp.route("/new", methods=['POST'])
 @csrf_protect(blueprint=bp)
 def update(uid=False):
-	return None
 	conn = get_conn()
-	is_newuser = bool(not uid)
-	if is_newuser:
-		user = User()
-		if not user.set_loginname(request.form['loginname']):
-			flash('Login name does not meet requirements')
-			return redirect(url_for('user.show'))
+	is_newmail = bool(not uid)
+	if is_newmail:
+		mail = Mail()
 	else:
-		conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
+		conn = get_conn()
+		conn.search(current_app.config["LDAP_BASE_MAIL"], '(&(objectclass=postfixVirtual)(uid={}))'.format((escape_filter_chars(uid))))
 		assert len(conn.entries) == 1
-		user = User.from_ldap(conn.entries[0])
-	if not user.set_mail(request.form['mail']):
-		flash('Mail is invalide.')
-		return redirect(url_for('user.show', uid=uid))
-	new_displayname = request.form['displayname'] if request.form['displayname'] else request.form['loginname']
-	if not user.set_displayname(new_displayname):
-		flash('Display name does not meet requirements')
-		return redirect(url_for('user.show', uid=uid))
-	new_password = request.form.get('password')
-	if new_password and not is_newuser:
-		user.set_password(new_password)
+		mail = Mail.from_ldap(conn.entries[0])
 
-	session = db.session
-	roles = Role.query.all()
-	for role in roles:
-		role_member_dns = role.member_dns()
-		if request.values.get('role-{}'.format(role.id), False) or role.name in current_app.config["ROLES_BASEROLES"]:
-			if user.dn in role_member_dns:
-				continue
-			role.add_member(user)
-		elif user.dn in role_member_dns:
-			role.del_member(user)
-	usergroups = set()
-	for role in Role.get_for_user(user).all():
-		usergroups.update(role.group_dns())
-	user.replace_group_dns(usergroups)
+	if is_newmail:
+		mail.uid = request.form.get('mail-uid')
+	mail.receivers = request.form.get('mail-receivers', '').splitlines();
+	mail.destinations = request.form.get('mail-destinations', '').splitlines();
 
-	if user.to_ldap(new=is_newuser):
-		if is_newuser:
-			send_passwordreset(user.loginname)
-			flash('User created. We sent the user a password reset link by mail')
-			session.commit()
-		else:
-			flash('User updated')
-			session.commit()
+	if mail.to_ldap(new=is_newmail):
+		flash('Mail mapping updated.')
 	else:
-		flash('Error updating user: {}'.format(conn.result['message']))
-		session.rollback()
-	return redirect(url_for('user.show', uid=user.uid))
+		flash('Error updating mail mapping: {}'.format(conn.result['message']))
+		if is_newmail:
+			return redirect(url_for('mail.index'))
+	return redirect(url_for('mail.show', uid=mail.uid))
 
 @bp.route("/<uid>/del")
 @csrf_protect(blueprint=bp)
 def delete(uid):
-	return None
 	conn = get_conn()
-	conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
+	conn.search(current_app.config["LDAP_BASE_MAIL"], '(&(objectclass=postfixVirtual)(uid={}))'.format((escape_filter_chars(uid))))
 	assert len(conn.entries) == 1
-	user = User.from_ldap(conn.entries[0])
-
-	session = db.session
-	roles = Role.query.all()
-	for role in roles:
-		if user.dn in role.member_dns():
-			role.del_member(user)
+	mail = conn.entries[0]
 
-	if conn.delete(conn.entries[0].entry_dn):
-		flash('Deleted user')
-		session.commit()
+	if conn.delete(mail.entry_dn):
+		flash('Deleted mail mapping.')
 	else:
-		flash('Could not delete user: {}'.format(conn.result['message']))
-		session.rollback()
-	return redirect(url_for('user.index'))
+		flash('Could not delete mail mapping: {}'.format(conn.result['message']))
+	return redirect(url_for('mail.index'))
-- 
GitLab