From 4042dabd35fdd1fab908efaa38b555fc1d58633b Mon Sep 17 00:00:00 2001
From: Michael Simon <simon@kit.edu>
Date: Fri, 21 Jun 2024 11:43:52 +0200
Subject: [PATCH] ISSUE-200 add emails from identity attribute sources to
 identity

---
 .../entity/identity/EmailAddressStatus.java   |   4 +-
 .../identity/IdentityEmailAddressService.java | 136 +------------
 .../scc/webreg/bean/EmailAddressesBean.java   |  43 +++-
 .../IdentityEmailAddressConverter.java        |  32 +++
 .../main/resources/msg/messages_de.properties |  20 +-
 .../main/resources/msg/messages_en.properties |  20 +-
 .../main/resources/msg/messages_fr.properties |  20 +-
 .../main/webapp/user/email-addresses.xhtml    |  80 ++++++--
 .../src/main/webapp/user/ssh-keys.xhtml       |  23 ++-
 regapp-idty/pom.xml                           |   5 +
 .../proc/EmailMergeValueProcessor.java        |  36 ++++
 .../proc/EmailToIdentityValueProcessor.java   |  81 ++++++++
 .../proc/IdentityValuesProcessor.java         |   4 +-
 .../proc/SingleStringMergeValueProcessor.java |   4 +-
 .../identity/IdentityEmailAddressHandler.java | 188 ++++++++++++++++++
 15 files changed, 525 insertions(+), 171 deletions(-)
 create mode 100644 bwreg-webapp/src/main/java/edu/kit/scc/webreg/converter/IdentityEmailAddressConverter.java
 create mode 100644 regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailMergeValueProcessor.java
 create mode 100644 regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailToIdentityValueProcessor.java
 create mode 100644 regapp-idty/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressHandler.java

diff --git a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/identity/EmailAddressStatus.java b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/identity/EmailAddressStatus.java
index 1d3d0a14f..e1418770a 100644
--- a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/identity/EmailAddressStatus.java
+++ b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/identity/EmailAddressStatus.java
@@ -3,5 +3,7 @@ package edu.kit.scc.webreg.entity.identity;
 public enum EmailAddressStatus {
 
 	VERIFIED,
-	UNVERIFIED
+	UNVERIFIED,
+	FROM_ATTRIBUTE_VERIFIED,
+	FROM_ATTRIBUTE_UNVERIFIED,
 }
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressService.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressService.java
index c398171f5..7230c21b3 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressService.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressService.java
@@ -1,170 +1,54 @@
 package edu.kit.scc.webreg.service.identity;
 
 import java.io.Serializable;
-import java.math.BigInteger;
-import java.security.SecureRandom;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
 
-import org.slf4j.Logger;
-
-import edu.kit.scc.regapp.mail.api.TemplateMailService;
-import edu.kit.scc.webreg.bootstrap.ApplicationConfig;
 import edu.kit.scc.webreg.dao.BaseDao;
 import edu.kit.scc.webreg.dao.identity.IdentityEmailAddressDao;
-import edu.kit.scc.webreg.dao.ops.RqlExpressions;
-import edu.kit.scc.webreg.entity.EventType;
-import edu.kit.scc.webreg.entity.identity.EmailAddressStatus;
 import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity;
-import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity_;
 import edu.kit.scc.webreg.entity.identity.IdentityEntity;
-import edu.kit.scc.webreg.event.EventSubmitter;
-import edu.kit.scc.webreg.event.IdentityEmailAddressEvent;
-import edu.kit.scc.webreg.event.exc.EventSubmitException;
 import edu.kit.scc.webreg.exc.VerificationException;
 import edu.kit.scc.webreg.service.impl.BaseServiceImpl;
 import jakarta.ejb.Stateless;
 import jakarta.inject.Inject;
 import jakarta.mail.internet.AddressException;
-import jakarta.mail.internet.InternetAddress;
 
 @Stateless
 public class IdentityEmailAddressService extends BaseServiceImpl<IdentityEmailAddressEntity> implements Serializable {
 
 	private static final long serialVersionUID = 1L;
 
-	@Inject
-	private Logger logger;
-
 	@Inject
 	private IdentityEmailAddressDao dao;
 
 	@Inject
-	private ApplicationConfig appConfig;
-
-	@Inject
-	private TemplateMailService mailService;
-
-	@Inject
-	private EventSubmitter eventSubmitter;
+	private IdentityEmailAddressHandler handler;
 
 	public IdentityEmailAddressEntity addEmailAddress(IdentityEntity identity, String emailAddress, String executor)
 			throws AddressException {
-		InternetAddress email = new InternetAddress(emailAddress, true);
-
-		IdentityEmailAddressEntity entity = dao.createNew();
-		entity.setIdentity(identity);
-		entity.setEmailAddress(email.getAddress());
-		entity.setVerificationToken(generateToken());
-		entity.setTokenValidUntil(generateTokenValidity());
-		entity.setEmailStatus(EmailAddressStatus.UNVERIFIED);
-		entity = dao.persist(entity);
 
-		sendVerificationEmail(entity);
-
-		IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity);
-		try {
-			eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_ADDED, executor);
-		} catch (EventSubmitException e) {
-			logger.warn("Could not submit event", e);
-		}
-
-		return entity;
+		return handler.addEmailAddress(identity, emailAddress, executor);
 	}
 
 	public void redoVerification(IdentityEmailAddressEntity entity, String executor) {
 		entity = dao.fetch(entity.getId());
 
-		entity.setVerificationToken(generateToken());
-		entity.setTokenValidUntil(generateTokenValidity());
-
-		sendVerificationEmail(entity);
-
-		IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity);
-		try {
-			eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_REDO_VERIFICATION, executor);
-		} catch (EventSubmitException e) {
-			logger.warn("Could not submit event", e);
-		}
+		handler.redoVerification(entity, executor);
 	}
 
 	public void deleteEmailAddress(IdentityEmailAddressEntity entity, String executor) {
 		entity = dao.fetch(entity.getId());
-		if (entity.equals(entity.getIdentity().getPrimaryEmail())) {
-			entity.getIdentity().setPrimaryEmail(null);
-		}
-		dao.delete(entity);
+		handler.deleteEmailAddress(entity, executor);
 	}
 
-	public IdentityEmailAddressEntity checkVerification(IdentityEntity identity, String token, String executor)
-			throws VerificationException {
-		IdentityEmailAddressEntity entity = dao
-				.find(RqlExpressions.equal(IdentityEmailAddressEntity_.verificationToken, token));
-
-		if (entity == null) {
-			throw new VerificationException("no_token_found");
-		}
-		
-		if (!identity.equals(entity.getIdentity())) {
-			throw new VerificationException("not_owner");
-		}
-
-		if (entity.getTokenValidUntil().before(new Date())) {
-			throw new VerificationException("token_expired");
-		}
-		
-		entity.setVerificationToken(null);
-		entity.setVerifiedOn(new Date());
-		entity.setValidUntil(generateValidity());
-		entity.setEmailStatus(EmailAddressStatus.VERIFIED);
-
-		IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity);
-		try {
-			eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_VERIFIED, executor);
-		} catch (EventSubmitException e) {
-			logger.warn("Could not submit event", e);
-		}
-		return entity;
-	}
-
-	private String generateToken() {
-		SecureRandom random = new SecureRandom();
-		String t = new BigInteger(130, random).toString(32);
-		return t;
-	}
-
-	private Date generateTokenValidity() {
-		Long validity = 30 * 60 * 1000L;
-		if (appConfig.getConfigValue("email_token_validity") != null) {
-			validity = Long.parseLong(appConfig.getConfigValue("email_token_validity"));
-		}
-
-		return new Date(System.currentTimeMillis() + validity);
-	}
-
-	private Date generateValidity() {
-		Long validity = 180 * 24 * 60 * 60 * 1000L;
-		if (appConfig.getConfigValue("email_validity") != null) {
-			validity = Long.parseLong(appConfig.getConfigValue("email_validity"));
-		}
-
-		return new Date(System.currentTimeMillis() + validity);
+	public void setPrimaryEmailAddress(IdentityEmailAddressEntity entity, String executor) {
+		entity = dao.fetch(entity.getId());
+		handler.setPrimaryEmailAddress(entity, executor);
 	}
 
-	protected void sendVerificationEmail(IdentityEmailAddressEntity emailAddress) {
-
-		logger.debug("Sending Email verification mail for identity {} (email: {})", emailAddress.getIdentity().getId(),
-				emailAddress.getEmailAddress());
-
-		String templateName = appConfig.getConfigValueOrDefault("email_verification_template", "email_verification");
-
-		Map<String, Object> context = new HashMap<String, Object>(3);
-		context.put("emailAddress", emailAddress);
-		context.put("identity", emailAddress.getIdentity());
+	public IdentityEmailAddressEntity checkVerification(IdentityEntity identity, String token, String executor)
+			throws VerificationException {
 
-		mailService.sendMail(templateName, context, true);
-		emailAddress.setVerificationSent(new Date());
+		return handler.checkVerification(identity, token, executor);
 	}
 
 	@Override
diff --git a/bwreg-webapp/src/main/java/edu/kit/scc/webreg/bean/EmailAddressesBean.java b/bwreg-webapp/src/main/java/edu/kit/scc/webreg/bean/EmailAddressesBean.java
index a08ad67e3..3e7d1dff3 100644
--- a/bwreg-webapp/src/main/java/edu/kit/scc/webreg/bean/EmailAddressesBean.java
+++ b/bwreg-webapp/src/main/java/edu/kit/scc/webreg/bean/EmailAddressesBean.java
@@ -11,7 +11,10 @@
 package edu.kit.scc.webreg.bean;
 
 import java.io.Serializable;
+import java.util.Comparator;
+import java.util.List;
 
+import edu.kit.scc.webreg.entity.identity.EmailAddressStatus;
 import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity;
 import edu.kit.scc.webreg.entity.identity.IdentityEntity;
 import edu.kit.scc.webreg.entity.identity.IdentityEntity_;
@@ -51,8 +54,13 @@ public class EmailAddressesBean implements Serializable {
 	private String addEmailAddress;
 
 	private String token;
-	
+	private List<IdentityEmailAddressEntity> primaryEmailList;
+	private IdentityEmailAddressEntity chosenPrimary;
+
 	public void preRenderView(ComponentSystemEvent ev) {
+		if (getToken() != null) {
+			checkVerification();
+		}
 	}
 
 	public void addEmailAddress() {
@@ -69,17 +77,27 @@ public class EmailAddressesBean implements Serializable {
 		service.deleteEmailAddress(address, "idty-" + session.getIdentityId());
 		identity = null;
 	}
-	
+
+	public void setPrimaryEmailAddress() {
+		service.setPrimaryEmailAddress(getChosenPrimary(), "idty-" + session.getIdentityId());
+		identity = null;
+		chosenPrimary = null;
+		messageGenerator.addResolvedInfoMessage("email_addresses.set_primary_email_success",
+				"email_addresses.set_primary_email_success_detail", true);
+	}
+
 	public void checkVerification() {
 		try {
 			service.checkVerification(getIdentity(), getToken(), "idty-" + session.getIdentityId());
 			token = null;
 			identity = null;
+			messageGenerator.addResolvedInfoMessage("email_addresses.verification_success",
+					"email_addresses.verification_success_detail", true);
 		} catch (VerificationException e) {
 			messageGenerator.addResolvedErrorMessage("email_addresses." + e.getMessage());
 		}
 	}
-	
+
 	public IdentityEntity getIdentity() {
 		if (identity == null) {
 			identity = identityService.findByIdWithAttrs(session.getIdentityId(), IdentityEntity_.emailAddresses);
@@ -102,4 +120,23 @@ public class EmailAddressesBean implements Serializable {
 	public void setToken(String token) {
 		this.token = token;
 	}
+
+	public List<IdentityEmailAddressEntity> getPrimaryEmailList() {
+		if (primaryEmailList == null) {
+			primaryEmailList = getIdentity().getEmailAddresses().stream()
+					.filter(e -> !EmailAddressStatus.UNVERIFIED.equals(e.getEmailStatus())).toList();
+		}
+		return primaryEmailList;
+	}
+
+	public IdentityEmailAddressEntity getChosenPrimary() {
+		if (chosenPrimary == null) {
+			chosenPrimary = getIdentity().getPrimaryEmail();
+		}
+		return chosenPrimary;
+	}
+
+	public void setChosenPrimary(IdentityEmailAddressEntity chosenPrimary) {
+		this.chosenPrimary = chosenPrimary;
+	}
 }
diff --git a/bwreg-webapp/src/main/java/edu/kit/scc/webreg/converter/IdentityEmailAddressConverter.java b/bwreg-webapp/src/main/java/edu/kit/scc/webreg/converter/IdentityEmailAddressConverter.java
new file mode 100644
index 000000000..010895ed1
--- /dev/null
+++ b/bwreg-webapp/src/main/java/edu/kit/scc/webreg/converter/IdentityEmailAddressConverter.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Michael Simon.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ * 
+ * Contributors:
+ *     Michael Simon - initial
+ ******************************************************************************/
+package edu.kit.scc.webreg.converter;
+
+import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity;
+import edu.kit.scc.webreg.service.BaseService;
+import edu.kit.scc.webreg.service.identity.IdentityEmailAddressService;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+
+@Named("identityEmailAddressConverter")
+public class IdentityEmailAddressConverter extends AbstractConverter<IdentityEmailAddressEntity> {
+
+	private static final long serialVersionUID = 1L;
+
+	@Inject
+	private IdentityEmailAddressService service;
+
+	@Override
+	protected BaseService<IdentityEmailAddressEntity> getService() {
+		return service;
+	}
+	
+}
diff --git a/bwreg-webapp/src/main/resources/msg/messages_de.properties b/bwreg-webapp/src/main/resources/msg/messages_de.properties
index 8740280e9..d2dc7e688 100644
--- a/bwreg-webapp/src/main/resources/msg/messages_de.properties
+++ b/bwreg-webapp/src/main/resources/msg/messages_de.properties
@@ -373,9 +373,23 @@ email_address = E-Mail-Adresse
 
 email_address_more = E-Mail-Adresse (weitere)
 
-email_addresses.heading              = Meine E-Mail Adressen
-email_addresses.no_primary_email_set = Keine gesetzt
-email_addresses.primary_email        = Prim\u00E4re E-Mail Adresse
+email_addresses.add_email_address           = Neue E-Mail Adresse hinzuf\u00FCgen
+email_addresses.from_attribute_source       = E-Mail Adresse von einer externen Quelle
+email_addresses.heading                     = Meine E-Mail Adressen
+email_addresses.no_primary_email_set        = Keine gesetzt
+email_addresses.no_token_found              = Dieses Token ist unbekannt
+email_addresses.not_owner                   = Falsches Token
+email_addresses.primary_email               = Prim\u00E4re E-Mail Adresse
+email_addresses.token_expired               = Das Token ist abgelaufen, bitte lassen Sie sich ein neues zusenden.
+email_addresses.token_sent                  = \u00DCberpr\u00FCfungstoken gesendet
+email_addresses.token_valid_until           = Token g\u00FCltig bis
+email_addresses.unverified                  = Noch nicht verifiziert
+email_addresses.verification_success        = Verifizierung erfolgreich
+email_addresses.verification_success_detail = Die \u00DCberpr\u00FCfung der E-Mail-Adresse war erfolgreich. Sie k\u00F6nnen sie nun verwenden.
+email_addresses.verification_token          = E-Mail Adresse best\u00E4tigen
+email_addresses.verification_valid_until    = G\u00FCltig bis
+email_addresses.verified                    = Verifiziert
+email_addresses.verified_on                 = Verifiziert am
 
 email_signature_keys = Schl\u00FCssel zum Signieren
 
diff --git a/bwreg-webapp/src/main/resources/msg/messages_en.properties b/bwreg-webapp/src/main/resources/msg/messages_en.properties
index 9d67eca36..298dc2c23 100644
--- a/bwreg-webapp/src/main/resources/msg/messages_en.properties
+++ b/bwreg-webapp/src/main/resources/msg/messages_en.properties
@@ -373,9 +373,23 @@ email_address = E-Mail address
 
 email_address_more = E-Mail address (more)
 
-email_addresses.heading              = My E-Mail-Addresses
-email_addresses.no_primary_email_set = None set
-email_addresses.primary_email        = Primary e-mail address
+email_addresses.add_email_address           = Add new e-mail address
+email_addresses.from_attribute_source       = E-mail address from an external source
+email_addresses.heading                     = My E-Mail-Addresses
+email_addresses.no_primary_email_set        = None set
+email_addresses.no_token_found              = This token is unknown
+email_addresses.not_owner                   = Token wrong
+email_addresses.primary_email               = Primary e-mail address
+email_addresses.token_expired               = The token has expired, please have a new one sent to you.
+email_addresses.token_sent                  = Verification token sent
+email_addresses.token_valid_until           = Token valid until
+email_addresses.unverified                  = Not yet verified
+email_addresses.verification_success        = Verification successful
+email_addresses.verification_success_detail = The verification of the e-mail address was successful. You can now use it.
+email_addresses.verification_token          = Confirm e-mail address
+email_addresses.verification_valid_until    = Valid until
+email_addresses.verified                    = Verified
+email_addresses.verified_on                 = Verified on
 
 email_signature_keys = Keys for signing emails
 
diff --git a/bwreg-webapp/src/main/resources/msg/messages_fr.properties b/bwreg-webapp/src/main/resources/msg/messages_fr.properties
index 8f3f60827..2ff5a7874 100644
--- a/bwreg-webapp/src/main/resources/msg/messages_fr.properties
+++ b/bwreg-webapp/src/main/resources/msg/messages_fr.properties
@@ -373,9 +373,23 @@ email_address = Adresse \u00E9lectronique
 
 email_address_more = Adresse \u00E9lectronique (plus)
 
-email_addresses.heading              = Mes adresses e-mail
-email_addresses.no_primary_email_set = Aucune d\u00E9finie
-email_addresses.primary_email        = Adresse e-mail primaire
+email_addresses.add_email_address           = Ajouter une nouvelle adresse e-mail
+email_addresses.from_attribute_source       = Adresse e-mail d'une source externe
+email_addresses.heading                     = Mes adresses e-mail
+email_addresses.no_primary_email_set        = Aucune d\u00E9finie
+email_addresses.no_token_found              = Ce token est inconnu
+email_addresses.not_owner                   = Mauvais token
+email_addresses.primary_email               = Adresse e-mail primaire
+email_addresses.token_expired               = Le token est expir\u00E9, veuillez vous en faire envoyer un nouveau.
+email_addresses.token_sent                  = Jeton de v\u00E9rification envoy\u00E9
+email_addresses.token_valid_until           = Token valable jusqu'au
+email_addresses.unverified                  = Pas encore v\u00E9rifi\u00E9
+email_addresses.verification_success        = V\u00E9rification r\u00E9ussie
+email_addresses.verification_success_detail = La v\u00E9rification de l'adresse e-mail a \u00E9t\u00E9 effectu\u00E9e avec succ\u00E8s. Vous pouvez maintenant l'utiliser.
+email_addresses.verification_token          = Confirmer l'adresse e-mail
+email_addresses.verification_valid_until    = Valable jusqu'au
+email_addresses.verified                    = V\u00E9rifi\u00E9
+email_addresses.verified_on                 = V\u00E9rifi\u00E9 le
 
 email_signature_keys = Cl\u00E9s pour signer des e-mails
 
diff --git a/bwreg-webapp/src/main/webapp/user/email-addresses.xhtml b/bwreg-webapp/src/main/webapp/user/email-addresses.xhtml
index 22b12b3ea..463f4cc3f 100644
--- a/bwreg-webapp/src/main/webapp/user/email-addresses.xhtml
+++ b/bwreg-webapp/src/main/webapp/user/email-addresses.xhtml
@@ -14,10 +14,11 @@
 <body>
 
 <f:view>
-<f:metadata>
-	<f:event type="jakarta.faces.event.PreRenderViewEvent"
-           listener="#{emailAddressesBean.preRenderView}" />
-</f:metadata>
+	<f:metadata>
+		<f:viewParam name="t" value="#{emailAddressesBean.token}"/>
+		<f:event type="jakarta.faces.event.PreRenderViewEvent"
+	           listener="#{emailAddressesBean.preRenderView}" />
+	</f:metadata>
 
 <ui:composition template="/template/default.xhtml">
 	<ui:param name="title" value="#{messages.title}"/>
@@ -29,25 +30,70 @@
 
 		<div><p:messages showDetail="true" /></div>
 
+		<div class="col-12 md:col-3 xl:col-3, col-12 md:col-9 xl:col-9">
+			<p:selectOneListbox id="userSelect" var="e" value="#{emailAddressesBean.chosenPrimary}" converter="#{identityEmailAddressConverter}" 
+					style="font-size: 1em;">
+				<f:selectItems value="#{emailAddressesBean.primaryEmailList}" var="email" itemLabel="#{email.emailAddress}" itemValue="#{email}" />
+				<p:column>
+				<h:outputText value="#{e.emailAddress}" />
+				</p:column>
+			</p:selectOneListbox>
+			<div class="form">
+				<p:commandButton action="#{emailAddressesBean.setPrimaryEmailAddress()}" value="#{messages['set']}" validateClient="true" ajax="true" update="@all" />
+			</div>
+		</div>
 		<div>
 			<h:outputText value="#{messages['email_addresses.primary_email']}: "/>
-			<h:outputText value="#{emailAddressesBean.identity.primaryEmail}" rendered="#{emailAddressesBean.identity.primaryEmail != null}"/>
+			<h:outputText value="#{emailAddressesBean.identity.primaryEmail.emailAddress}" rendered="#{emailAddressesBean.identity.primaryEmail != null}"/>
 			<h:outputText value="#{messages['email_addresses.no_primary_email_set']}" rendered="#{emailAddressesBean.identity.primaryEmail == null}"/>
 		</div>
-
+		
 		<ui:repeat var="email" value="#{emailAddressesBean.identity.emailAddresses}">
-			<div>
-				<h:outputText value="#{email.emailAddress}" />, 
-				<h:outputText value="#{email.emailStatus}" />, 
-				<h:outputText value="#{email.tokenValidUntil}" />, 
-				<h:outputText value="#{email.verificationSent}" />, 
-				<h:outputText value="#{email.verifiedOn}" />,
-				<h:outputText value="#{email.validUntil}" />, 
-				<p:commandLink action="#{emailAddressesBean.deleteEmailAddress(email)}" value="#{messages['delete']}" update="@all"/>
-			</div>
+			<h:panelGroup layout="block" styleClass="col-12 md:col-3 xl:col-3, col-12 md:col-9 xl:col-9" 
+					style="margin: 1em; background-color: #f8f9fa;border: 1px solid #dee2e6;">
+	        	<div>
+	        		<b><h:outputText value="#{email.emailAddress}"/></b>
+				</div>
+				<h:panelGroup rendered="#{email.emailStatus == 'FROM_ATTRIBUTE_UNVERIFIED'}">
+					<h:outputText value="#{messages['email_addresses.from_attribute_source']}"/>
+				</h:panelGroup>
+				<h:panelGroup rendered="#{email.emailStatus == 'FROM_ATTRIBUTE_VERIFIED'}">
+					<h:outputText value="#{messages['email_addresses.from_attribute_source']}"/>
+				</h:panelGroup>
+				<h:panelGroup rendered="#{email.emailStatus == 'UNVERIFIED'}">
+					<div>
+						<h:outputText value="#{messages['email_addresses.unverified']}"/>
+					</div>
+					<div>
+						<h:outputText value="#{messages['email_addresses.token_sent']} "/><h:outputText value="#{of:formatDate(email.verificationSent, 'dd.MM.yyyy HH:mm')}"/>
+					</div>
+					<div>
+						<h:outputText value="#{messages['email_addresses.token_valid_until']}"/> #{of:formatDate(email.tokenValidUntil, 'dd.MM.yyyy HH:mm')} 
+					</div>
+					
+					<div class="form">
+						<p:commandButton action="#{emailAddressesBean.deleteEmailAddress(email)}" value="#{messages['delete']}" ajax="true" update="@all" />
+					</div>
+				</h:panelGroup>
+				<h:panelGroup rendered="#{email.emailStatus == 'VERIFIED'}">
+					<div>
+					<h:outputText value="#{messages['email_addresses.verified']}"/>
+					</div>
+					<div>
+					<h:outputText value="#{messages['email_addresses.verified_on']} "/><h:outputText value="#{of:formatDate(email.verifiedOn, 'dd.MM.yyyy HH:mm')}"/> 
+					</div>
+					<div>
+					<h:outputText value="#{messages['email_addresses.verification_valid_until']} "/> <h:outputText value="#{of:formatDate(email.validUntil, 'dd.MM.yyyy HH:mm')}"/> 
+					</div>
+					
+					<div class="form">
+						<p:commandButton action="#{emailAddressesBean.deleteEmailAddress(email)}" value="#{messages['delete']}" ajax="true" update="@all" />
+					</div>
+				</h:panelGroup>
+			</h:panelGroup>
 		</ui:repeat>
 
-		<div>
+		<div class="col-12 md:col-3 xl:col-3, col-12 md:col-9 xl:col-9">
 			<h:outputText value="#{messages['email_addresses.add_email_address']}: "/>
 			<p:inputText type="email" value="#{emailAddressesBean.addEmailAddress}" />
 			<div class="form">
@@ -55,7 +101,7 @@
 			</div>
 		</div>
 	
-		<div>
+		<div class="col-12 md:col-3 xl:col-3, col-12 md:col-9 xl:col-9">
 			<h:outputText value="#{messages['email_addresses.verification_token']}: "/>
 			<p:inputText value="#{emailAddressesBean.token}" />
 			<div class="form">
diff --git a/bwreg-webapp/src/main/webapp/user/ssh-keys.xhtml b/bwreg-webapp/src/main/webapp/user/ssh-keys.xhtml
index 4a3eb120c..18d434b89 100644
--- a/bwreg-webapp/src/main/webapp/user/ssh-keys.xhtml
+++ b/bwreg-webapp/src/main/webapp/user/ssh-keys.xhtml
@@ -23,7 +23,7 @@
 	<ui:param name="title" value="#{messages.title}"/>
 
 	<ui:define name="content">
-	<h:form id="form" class="form full fancy">
+	<h:form id="form" class="full">
 
 		<h3>#{messages['ssh_keys.key_list']}</h3>
 
@@ -33,7 +33,7 @@
 				columns="2" layout="grid" style="margin-bottom: 16px;">
 	        <p:panel styleClass="grayback" style="margin-bottom: 0px;">
 	        	<f:facet name="header">
-	        		<i class="fa fa-fw fa-key"></i>
+	        		<i class="pi pi-key"></i>
 	        		<b><h:outputText value="#{key.pubKeyEntity.name}"/></b>
 				</f:facet>
 				
@@ -70,18 +70,19 @@
 	        		
 	        	</h:panelGrid>
 
-				<p:commandButton action="#{userSshKeyManagementBean.deleteKey(key.pubKeyEntity.name)}" value="#{messages.revoke}" 
-					update="@form" immediate="true">
-					<p:confirm header="#{messages.confirm_header}" message="#{messages.ssh_key_confirm}" />
-				</p:commandButton>
-
+				<div class="form">
+					<p:commandButton action="#{userSshKeyManagementBean.deleteKey(key.pubKeyEntity.name)}" value="#{messages.revoke}" 
+						update="@form" immediate="true" style="font-size: 1rem;">
+						<p:confirm header="#{messages.confirm_header}" message="#{messages.ssh_key_confirm}" />
+					</p:commandButton>
+				</div>
 			</p:panel>		
 		</p:dataGrid>
 
-		<p:panel>
-			<p:commandButton id="openAddSshKeyDlg" oncomplete="PF('addSshKeyDlg').show();" value="#{messages.add_ssh_pub_key}"></p:commandButton>
-		</p:panel>
-
+		<div class="form">
+			<p:commandButton id="openAddSshKeyDlg" oncomplete="PF('addSshKeyDlg').show();" value="#{messages.add_ssh_pub_key}" style="font-size: 1rem;"></p:commandButton>
+		</div>
+		
 		<p:dialog header="#{messages.add_ssh_pub_key}" 
 					widgetVar="addSshKeyDlg" id="addSshKeyDlgId" modal="true" closable="true" closeOnEscape="true"
 					showEffect="fade" hideEffect="fade">
diff --git a/regapp-idty/pom.xml b/regapp-idty/pom.xml
index f886842e3..681b88143 100644
--- a/regapp-idty/pom.xml
+++ b/regapp-idty/pom.xml
@@ -91,6 +91,11 @@
 			<artifactId>regapp-as</artifactId>
 			<version>${project.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>edu.kit.scc</groupId>
+			<artifactId>regapp-mail</artifactId>
+			<version>${project.version}</version>
+		</dependency>
 
 		<dependency>
 			<groupId>jakarta.platform</groupId>
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailMergeValueProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailMergeValueProcessor.java
new file mode 100644
index 000000000..645538d9b
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailMergeValueProcessor.java
@@ -0,0 +1,36 @@
+package edu.kit.scc.webreg.service.attribute.proc;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import edu.kit.scc.webreg.entity.attribute.IdentityAttributeSetEntity;
+import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity;
+import edu.kit.scc.webreg.entity.identity.IdentityEntity;
+import edu.kit.scc.webreg.service.identity.IdentityAttributeResolver;
+import jakarta.enterprise.inject.spi.CDI;
+
+public class EmailMergeValueProcessor extends SingleStringMergeValueProcessor {
+
+	public EmailMergeValueProcessor(String outputAttribute, String... inspectValues) {
+		super(outputAttribute, inspectValues);
+	}
+
+	public void apply(IdentityAttributeSetEntity attributeSet) {
+		super.apply(attributeSet);
+
+		IdentityEntity identity = attributeSet.getIdentity();
+		if (identity.getPrimaryEmail() == null) {
+			String email = getIdentityAttributeResolver().resolveSingleStringValue(identity, outputAttribute);
+			Map<String, IdentityEmailAddressEntity> emailMap = identity.getEmailAddresses().stream()
+					.collect(Collectors.toMap(IdentityEmailAddressEntity::getEmailAddress, Function.identity()));
+			if (emailMap.containsKey(email)) {
+				identity.setPrimaryEmail(emailMap.get(email));
+			}
+		}
+	}
+	
+	private IdentityAttributeResolver getIdentityAttributeResolver() {
+		return CDI.current().select(IdentityAttributeResolver.class).get();
+	}
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailToIdentityValueProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailToIdentityValueProcessor.java
new file mode 100644
index 000000000..fc3c91beb
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/EmailToIdentityValueProcessor.java
@@ -0,0 +1,81 @@
+package edu.kit.scc.webreg.service.attribute.proc;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import edu.kit.scc.webreg.entity.attribute.IdentityAttributeSetEntity;
+import edu.kit.scc.webreg.entity.attribute.value.StringListValueEntity;
+import edu.kit.scc.webreg.entity.attribute.value.StringValueEntity;
+import edu.kit.scc.webreg.entity.attribute.value.ValueEntity;
+import edu.kit.scc.webreg.entity.identity.EmailAddressStatus;
+import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity;
+import edu.kit.scc.webreg.entity.identity.IdentityEntity;
+import edu.kit.scc.webreg.service.identity.IdentityEmailAddressHandler;
+import jakarta.enterprise.inject.spi.CDI;
+import jakarta.mail.internet.AddressException;
+
+public class EmailToIdentityValueProcessor extends StringListMergeValueProcessor {
+
+	public EmailToIdentityValueProcessor(String outputAttribute, String... inspectValues) {
+		super(outputAttribute, inspectValues);
+	}
+
+	public void apply(IdentityAttributeSetEntity attributeSet) {
+		super.apply(attributeSet);
+
+		Set<String> emailAddresses = new HashSet<>();
+		for (ValueEntity value : getValueList()) {
+			if (value instanceof StringValueEntity) {
+				emailAddresses.add(((StringValueEntity) value).getValueString());
+			} else if (value instanceof StringListValueEntity) {
+				if (((StringListValueEntity) value).getValueList() != null) {
+					emailAddresses.addAll(((StringListValueEntity) value).getValueList());
+				}
+			}
+		}
+
+		IdentityEntity identity = attributeSet.getIdentity();
+		Map<String, IdentityEmailAddressEntity> emailMap = identity.getEmailAddresses().stream()
+				.collect(Collectors.toMap(IdentityEmailAddressEntity::getEmailAddress, Function.identity()));
+		
+		for (String email : emailAddresses) {
+			// Add email addresses from attribute sources to identity
+			if (! emailMap.containsKey(email)) {
+				createIdentityEmailAddress(identity, email);
+			}
+		}
+		for (Entry<String, IdentityEmailAddressEntity> entry : emailMap.entrySet()) {
+			// Remove email addresses which are attribute types and no longer there
+			if (EmailAddressStatus.FROM_ATTRIBUTE_UNVERIFIED.equals(entry.getValue().getEmailStatus()) ||
+					EmailAddressStatus.FROM_ATTRIBUTE_VERIFIED.equals(entry.getValue().getEmailStatus())) {
+				if (! emailAddresses.contains(entry.getValue().getEmailAddress())) {
+					deleteIdentityEmailAddress(identity, entry.getValue());
+				}
+			}
+		}		
+	}
+	
+	private void deleteIdentityEmailAddress(IdentityEntity identity, IdentityEmailAddressEntity email) {
+		IdentityEmailAddressHandler handler = getIdentityEmailAddressHandler();
+		if (identity.getPrimaryEmail().equals(email))
+			identity.setPrimaryEmail(null);
+		handler.deleteEmailAddress(email, "idty-" + identity.getId());
+	}
+	
+	private void createIdentityEmailAddress(IdentityEntity identity, String email) {
+		IdentityEmailAddressHandler handler = getIdentityEmailAddressHandler();
+		try {
+			handler.addEmailAddressFromAttribute(identity, email, "idty-" + identity.getId());
+		} catch (AddressException e) {
+			logger.info("Unparsable email address: {} error: {}", email, e.getMessage());
+		}
+	}
+	
+	private IdentityEmailAddressHandler getIdentityEmailAddressHandler() {
+		return CDI.current().select(IdentityEmailAddressHandler.class).get();
+	}
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java
index 937e5bc4f..e95628c88 100644
--- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java
@@ -24,8 +24,8 @@ public class IdentityValuesProcessor {
 	}
 
 	private List<ValueProcessor> loadProcessors() {
-		return Arrays.asList(new StringListMergeValueProcessor("email_all", "email"),
-				new SingleStringMergeValueProcessor("email", "email"),
+		return Arrays.asList(new EmailToIdentityValueProcessor("email_all", "email"),
+				new EmailMergeValueProcessor("email", "email"),
 				new StringListMergeValueProcessor("voperson_external_affiliation", "eduperson_affiliation"),
 				new StringListMergeValueProcessor("eduperson_assurance", "eduperson_assurance"),
 				new StringListMergeAuthorityValueProcessor("eduperson_entitlement", "eduperson_entitlement"),
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/SingleStringMergeValueProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/SingleStringMergeValueProcessor.java
index 22f331a94..36cb36379 100644
--- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/SingleStringMergeValueProcessor.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/SingleStringMergeValueProcessor.java
@@ -17,8 +17,8 @@ import edu.kit.scc.webreg.entity.attribute.value.ValueEntity;
 
 public class SingleStringMergeValueProcessor extends AbstractListProcessor {
 	
-	private String outputAttribute;
-	private String[] inspectValues;
+	protected String outputAttribute;
+	protected String[] inspectValues;
 	
 	public SingleStringMergeValueProcessor(String outputAttribute, String... inspectValues) {
 		this.outputAttribute = outputAttribute;
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressHandler.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressHandler.java
new file mode 100644
index 000000000..e4f1b1648
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/identity/IdentityEmailAddressHandler.java
@@ -0,0 +1,188 @@
+package edu.kit.scc.webreg.service.identity;
+
+import java.io.Serializable;
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+
+import edu.kit.scc.regapp.mail.impl.TemplateMailSender;
+import edu.kit.scc.webreg.bootstrap.ApplicationConfig;
+import edu.kit.scc.webreg.dao.identity.IdentityEmailAddressDao;
+import edu.kit.scc.webreg.dao.ops.RqlExpressions;
+import edu.kit.scc.webreg.entity.EventType;
+import edu.kit.scc.webreg.entity.identity.EmailAddressStatus;
+import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity;
+import edu.kit.scc.webreg.entity.identity.IdentityEmailAddressEntity_;
+import edu.kit.scc.webreg.entity.identity.IdentityEntity;
+import edu.kit.scc.webreg.event.EventSubmitter;
+import edu.kit.scc.webreg.event.IdentityEmailAddressEvent;
+import edu.kit.scc.webreg.event.exc.EventSubmitException;
+import edu.kit.scc.webreg.exc.VerificationException;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.mail.internet.AddressException;
+import jakarta.mail.internet.InternetAddress;
+
+@ApplicationScoped
+public class IdentityEmailAddressHandler implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private IdentityEmailAddressDao dao;
+
+	@Inject
+	private ApplicationConfig appConfig;
+
+	@Inject
+	private TemplateMailSender mailService;
+
+	@Inject
+	private EventSubmitter eventSubmitter;
+
+	public void setPrimaryEmailAddress(IdentityEmailAddressEntity entity, String executor) {
+		entity.getIdentity().setPrimaryEmail(entity);
+	}
+	
+	public IdentityEmailAddressEntity addEmailAddressFromAttribute(IdentityEntity identity, String emailAddress, String executor)
+			throws AddressException {
+		InternetAddress email = new InternetAddress(emailAddress, true);
+
+		IdentityEmailAddressEntity entity = dao.createNew();
+		entity.setIdentity(identity);
+		entity.setEmailAddress(email.getAddress());
+		entity.setEmailStatus(EmailAddressStatus.FROM_ATTRIBUTE_UNVERIFIED);
+		entity = dao.persist(entity);
+
+		return entity;
+		
+	}
+	
+	public IdentityEmailAddressEntity addEmailAddress(IdentityEntity identity, String emailAddress, String executor)
+			throws AddressException {
+		InternetAddress email = new InternetAddress(emailAddress, true);
+
+		IdentityEmailAddressEntity entity = dao.createNew();
+		entity.setIdentity(identity);
+		entity.setEmailAddress(email.getAddress());
+		entity.setVerificationToken(generateToken());
+		entity.setTokenValidUntil(generateTokenValidity());
+		entity.setEmailStatus(EmailAddressStatus.UNVERIFIED);
+		entity = dao.persist(entity);
+
+		sendVerificationEmail(entity);
+
+		IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity);
+		try {
+			eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_ADDED, executor);
+		} catch (EventSubmitException e) {
+			logger.warn("Could not submit event", e);
+		}
+
+		return entity;
+	}
+
+	public void redoVerification(IdentityEmailAddressEntity entity, String executor) {
+		entity.setVerificationToken(generateToken());
+		entity.setTokenValidUntil(generateTokenValidity());
+
+		sendVerificationEmail(entity);
+
+		IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity);
+		try {
+			eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_REDO_VERIFICATION, executor);
+		} catch (EventSubmitException e) {
+			logger.warn("Could not submit event", e);
+		}
+	}
+
+	public void deleteEmailAddress(IdentityEmailAddressEntity entity, String executor) {
+		if (entity.equals(entity.getIdentity().getPrimaryEmail())) {
+			entity.getIdentity().setPrimaryEmail(null);
+		}
+		dao.delete(entity);
+	}
+
+	public IdentityEmailAddressEntity checkVerification(IdentityEntity identity, String token, String executor)
+			throws VerificationException {
+		IdentityEmailAddressEntity entity = dao
+				.find(RqlExpressions.equal(IdentityEmailAddressEntity_.verificationToken, token));
+
+		if (entity == null) {
+			throw new VerificationException("no_token_found");
+		}
+		
+		if (!identity.equals(entity.getIdentity())) {
+			throw new VerificationException("not_owner");
+		}
+
+		if (entity.getTokenValidUntil().before(new Date())) {
+			throw new VerificationException("token_expired");
+		}
+		
+		entity.setVerificationToken(null);
+		entity.setVerifiedOn(new Date());
+		entity.setValidUntil(generateValidity());
+		entity.setEmailStatus(EmailAddressStatus.VERIFIED);
+
+		if (identity.getPrimaryEmail() == null) {
+			// use identity from jpa session, the object from method call is detached
+			identity = entity.getIdentity();
+			identity.setPrimaryEmail(entity);
+		}
+		
+		IdentityEmailAddressEvent event = new IdentityEmailAddressEvent(entity);
+		try {
+			eventSubmitter.submit(event, EventType.EMAIL_ADDRESS_VERIFIED, executor);
+		} catch (EventSubmitException e) {
+			logger.warn("Could not submit event", e);
+		}
+		return entity;
+	}
+
+	private String generateToken() {
+		SecureRandom random = new SecureRandom();
+		String t = new BigInteger(130, random).toString(32);
+		return t;
+	}
+
+	private Date generateTokenValidity() {
+		Long validity = 30 * 60 * 1000L;
+		if (appConfig.getConfigValue("email_token_validity") != null) {
+			validity = Long.parseLong(appConfig.getConfigValue("email_token_validity"));
+		}
+
+		return new Date(System.currentTimeMillis() + validity);
+	}
+
+	private Date generateValidity() {
+		Long validity = 180 * 24 * 60 * 60 * 1000L;
+		if (appConfig.getConfigValue("email_validity") != null) {
+			validity = Long.parseLong(appConfig.getConfigValue("email_validity"));
+		}
+
+		return new Date(System.currentTimeMillis() + validity);
+	}
+
+	protected void sendVerificationEmail(IdentityEmailAddressEntity emailAddress) {
+
+		logger.debug("Sending Email verification mail for identity {} (email: {})", emailAddress.getIdentity().getId(),
+				emailAddress.getEmailAddress());
+
+		String templateName = appConfig.getConfigValueOrDefault("email_verification_template", "email_verification");
+
+		Map<String, Object> context = new HashMap<String, Object>(3);
+		context.put("emailAddress", emailAddress);
+		context.put("identity", emailAddress.getIdentity());
+
+		mailService.sendMail(templateName, context, true);
+		emailAddress.setVerificationSent(new Date());
+	}
+}
-- 
GitLab