This is an automated email from the git hooks/post-receive script. New commit to branch develop in repository pollen. See https://gitlab.nuiton.org/chorem/pollen.git commit 772268b108a394c10d8288b790776d2ec134fc02 Author: Sylvain Bavencoff <bavencoff@codelutin.com> Date: Wed May 30 10:39:24 2018 +0200 refs #79 : envoie de mail chiffré --- .../h2/V3_2_0_2__add_PGP_public_key_in_email.sql | 2 + .../V3_2_0_2__add_PGP_public_key_in_email.sql | 2 + pollen-persistence/src/main/xmi/pollen.properties | 2 +- pollen-persistence/src/main/xmi/pollen.zargo | Bin 30846 -> 30924 bytes .../rest/api/PollenRestApiRequestFilter.java | 4 +- .../chorem/pollen/rest/api/v1/PollenUserApi.java | 18 ++ pollen-services/pom.xml | 9 + .../services/bean/PollenUserEmailAddressBean.java | 10 ++ .../pollen/services/service/CryptoService.java | 183 +++++++++++++++++++++ .../services/service/PollenServiceSupport.java | 4 + .../pollen/services/service/PollenUserService.java | 34 ++++ .../pollen/services/service/mail/EmailService.java | 11 +- pollen-ui-riot-js/src/main/web/i18n/fr.json | 13 +- pollen-ui-riot-js/src/main/web/js/UserService.js | 11 ++ .../src/main/web/tag/UserProfile.tag.html | 8 - .../tag/components/UserEmailAddressList.tag.html | 172 +++++++++++++++---- pom.xml | 13 ++ 17 files changed, 450 insertions(+), 46 deletions(-) diff --git a/pollen-persistence/src/main/resources/db/migration/h2/V3_2_0_2__add_PGP_public_key_in_email.sql b/pollen-persistence/src/main/resources/db/migration/h2/V3_2_0_2__add_PGP_public_key_in_email.sql new file mode 100644 index 00000000..9d348609 --- /dev/null +++ b/pollen-persistence/src/main/resources/db/migration/h2/V3_2_0_2__add_PGP_public_key_in_email.sql @@ -0,0 +1,2 @@ +-- add invitation sent in VoterListMember +alter table pollenuserEmailaddress add pgppublickey LONGVARCHAR; \ No newline at end of file diff --git a/pollen-persistence/src/main/resources/db/migration/postgresql/V3_2_0_2__add_PGP_public_key_in_email.sql b/pollen-persistence/src/main/resources/db/migration/postgresql/V3_2_0_2__add_PGP_public_key_in_email.sql new file mode 100644 index 00000000..d0ea8eb2 --- /dev/null +++ b/pollen-persistence/src/main/resources/db/migration/postgresql/V3_2_0_2__add_PGP_public_key_in_email.sql @@ -0,0 +1,2 @@ +-- add invitation sent in VoterListMember +alter table pollenuserEmailaddress add pgppublickey TEXT; diff --git a/pollen-persistence/src/main/xmi/pollen.properties b/pollen-persistence/src/main/xmi/pollen.properties index 4c4c9289..8001c99f 100644 --- a/pollen-persistence/src/main/xmi/pollen.properties +++ b/pollen-persistence/src/main/xmi/pollen.properties @@ -18,7 +18,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # #L% ###m -model.tagvalue.version=3.2.0.1 +model.tagvalue.version=3.2.0.2 #model.tagValue.notGenerateToString=true #model.tagValue.constantPrefix=PROPERTY_ #model.tagValue.useEnumerationName=true diff --git a/pollen-persistence/src/main/xmi/pollen.zargo b/pollen-persistence/src/main/xmi/pollen.zargo index 18196270..52b95159 100644 Binary files a/pollen-persistence/src/main/xmi/pollen.zargo and b/pollen-persistence/src/main/xmi/pollen.zargo differ diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiRequestFilter.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiRequestFilter.java index c7a03a06..ce6e30b6 100644 --- a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiRequestFilter.java +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiRequestFilter.java @@ -36,6 +36,7 @@ import org.chorem.pollen.services.PollenServiceContext; import org.chorem.pollen.services.PollenUIContext; import org.chorem.pollen.services.service.ChoiceService; import org.chorem.pollen.services.service.CommentService; +import org.chorem.pollen.services.service.CryptoService; import org.chorem.pollen.services.service.FavoriteListService; import org.chorem.pollen.services.service.FeedService; import org.chorem.pollen.services.service.FeedbackService; @@ -138,7 +139,8 @@ public class PollenRestApiRequestFilter implements ContainerRequestFilter, Conta SocialAuthService.class, GtuService.class, TransverseService.class, - MailBoxService.class); + MailBoxService.class, + CryptoService.class); /** Logger. */ diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/PollenUserApi.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/PollenUserApi.java index d3e19227..ef2fa799 100644 --- a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/PollenUserApi.java +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/PollenUserApi.java @@ -33,6 +33,7 @@ import org.chorem.pollen.services.bean.PaginationResultBean; import org.chorem.pollen.services.bean.PollenEntityId; import org.chorem.pollen.services.bean.PollenEntityRef; import org.chorem.pollen.services.bean.PollenUserBean; +import org.chorem.pollen.services.bean.PollenUserEmailAddressBean; import org.chorem.pollen.services.bean.resource.ResourceFileBean; import org.chorem.pollen.services.bean.resource.ResourceStreamBean; import org.chorem.pollen.services.service.InvalidFormException; @@ -273,4 +274,21 @@ public class PollenUserApi { throws PollenDefaultEmailAddressException { pollenUserService.removeEmailAddress(userId.getEntityId(), emailAddressId.getEntityId()); } + + @Path("/users/{userId}/email/{emailAddressId}") + @POST + public PollenUserEmailAddressBean saveEmailAddress(@Context PollenUserService pollenUserService, + @PathParam("userId") PollenEntityId<PollenUser> userId, + PollenUserEmailAddressBean emailAddress) + throws PollenDefaultEmailAddressException { + return pollenUserService.editEmailAddress(userId.getEntityId(), emailAddress); + } + + @Path("/user/email/{emailAddressId}") + @POST + public PollenUserEmailAddressBean saveEmailAddress(@Context PollenUserService pollenUserService, + PollenUserEmailAddressBean emailAddress) + throws PollenDefaultEmailAddressException { + return pollenUserService.editEmailAddress(emailAddress); + } } diff --git a/pollen-services/pom.xml b/pollen-services/pom.xml index e8d1d3a4..30c9ff2d 100644 --- a/pollen-services/pom.xml +++ b/pollen-services/pom.xml @@ -224,6 +224,15 @@ <artifactId>socialauth</artifactId> </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpg-jdk15on</artifactId> + </dependency> + </dependencies> <build> diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/bean/PollenUserEmailAddressBean.java b/pollen-services/src/main/java/org/chorem/pollen/services/bean/PollenUserEmailAddressBean.java index 24391a5e..a976447d 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/bean/PollenUserEmailAddressBean.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/bean/PollenUserEmailAddressBean.java @@ -32,6 +32,8 @@ public class PollenUserEmailAddressBean extends PollenBean<PollenUserEmailAddres protected boolean validated; + protected String pgpPublicKey; + public PollenUserEmailAddressBean() { super(PollenUserEmailAddress.class); } @@ -51,4 +53,12 @@ public class PollenUserEmailAddressBean extends PollenBean<PollenUserEmailAddres public void setValidated(boolean validated) { this.validated = validated; } + + public String getPgpPublicKey() { + return pgpPublicKey; + } + + public void setPgpPublicKey(String pgpPublicKey) { + this.pgpPublicKey = pgpPublicKey; + } } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/CryptoService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/CryptoService.java new file mode 100644 index 00000000..e60e0ec6 --- /dev/null +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/CryptoService.java @@ -0,0 +1,183 @@ +package org.chorem.pollen.services.service; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; +import org.chorem.pollen.persistence.entity.PollenUserEmailAddress; +import org.chorem.pollen.services.PollenTechnicalException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.security.Security; +import java.util.Date; +import java.util.Iterator; +import java.util.Optional; + +public class CryptoService extends PollenServiceSupport { + + private static final Log log = LogFactory.getLog(CryptoService.class); + + public static final String PROVIDER = "BC"; + + static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + public PGPPublicKey getPublicKey(String email) { + + PGPPublicKey pgpPublicKey = null; + + Optional<String> publicKeyString = getPollenUserEmailAddressDao() + .forEmailAddressEquals(email) + .tryFindUnique() + .toJavaUtil() + .map(PollenUserEmailAddress::getPgpPublicKey); + if (publicKeyString.isPresent()) { + try { + InputStream publicKeyInput = new ByteArrayInputStream(publicKeyString.get().getBytes()); + + pgpPublicKey = readPublicKey(publicKeyInput); + + } catch (IOException | PGPException e) { + throw new PollenTechnicalException("unable to read PGP public key for email : " + email, e); + } + + } + + return pgpPublicKey; + } + + protected PGPPublicKey readPublicKey(InputStream input) throws IOException, PGPException { + PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection( + PGPUtil.getDecoderStream(input), new JcaKeyFingerprintCalculator()); + + // + // we just loop through the collection till we find a key suitable for encryption, in the real + // world you would probably want to be a bit smarter about this. + // + + Iterator keyRingIter = pgpPub.getKeyRings(); + while (keyRingIter.hasNext()) { + PGPPublicKeyRing keyRing = (PGPPublicKeyRing)keyRingIter.next(); + + Iterator keyIter = keyRing.getPublicKeys(); + while (keyIter.hasNext()) + { + PGPPublicKey key = (PGPPublicKey)keyIter.next(); + + if (key.isEncryptionKey()) + { + return key; + } + } + } + + throw new IllegalArgumentException("Can't find encryption key in key ring."); + } + + protected byte[] compressMessage(byte[] message) throws IOException { + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream cos = comData.open(bOut); // open it with the final destination + + PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator(); + + // we want to generate compressed data. This might be a user option later, + // in which case we would pass in bOut. + OutputStream pOut = lData.open(cos, // the compressed output stream + PGPLiteralData.BINARY, + PGPLiteralDataGenerator.CONSOLE, // "filename" to store + message.length, // length of clear data + new Date() // current time + ); + + pOut.write(message); + pOut.close(); + + comData.close(); + + return bOut.toByteArray(); + } + + protected byte[] encryptMessage(byte[] message, PGPPublicKey pgpPublicKey) throws IOException, PGPException { + + if (Security.getProvider(PROVIDER) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + byte[] compressMessage = compressMessage(message); + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + + OutputStream out = new ArmoredOutputStream(bout); + + PGPEncryptedDataGenerator encryptedDataGenerator = createEncryptedDataGenerator(pgpPublicKey); + + OutputStream encOut = encryptedDataGenerator.open(out, compressMessage.length); + + encOut.write(compressMessage); + encOut.close(); + out.close(); + + return bout.toByteArray(); + + } + + private PGPEncryptedDataGenerator createEncryptedDataGenerator(PGPPublicKey pgpPublicKey) { + JcePGPDataEncryptorBuilder encryptorBuilder = new JcePGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.CAST5) + .setSecureRandom(SECURE_RANDOM) + .setWithIntegrityPacket(true) + .setProvider(PROVIDER); + + PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(encryptorBuilder); + + JcePublicKeyKeyEncryptionMethodGenerator method = new JcePublicKeyKeyEncryptionMethodGenerator(pgpPublicKey) + .setProvider(PROVIDER) + .setSecureRandom(SECURE_RANDOM); + + encryptedDataGenerator.addMethod(method); + + return encryptedDataGenerator; + } + + public String encryptMailIfAsKey(String body, String email) { + + PGPPublicKey publicKey = getPublicKey(email); + + if (publicKey != null) { + + try { + byte[] encryptMessage = encryptMessage(body.getBytes(), publicKey); + return new String(encryptMessage); + } catch (IOException | PGPException e) { + if (log.isErrorEnabled()) { + log.error("error on encrypte mail to email", e); + } + return body; + } + + } + + return body; + + } + +} diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/PollenServiceSupport.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/PollenServiceSupport.java index d46beb01..6008b407 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/service/PollenServiceSupport.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/PollenServiceSupport.java @@ -188,6 +188,10 @@ public abstract class PollenServiceSupport implements PollenService { return newService(VoteService.class); } + protected CryptoService getCryptoService() { + return newService(CryptoService.class); + } + protected <E extends PollenService> E newService(Class<E> serviceClass) { return serviceContext.newService(serviceClass); } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/PollenUserService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/PollenUserService.java index 39318bb4..e6d4dabb 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/service/PollenUserService.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/PollenUserService.java @@ -109,6 +109,7 @@ public class PollenUserService extends PollenServiceSupport implements PollenSer bean.setEntityId(entity.getTopiaId()); bean.setEmailAddress(entity.getEmailAddress()); bean.setValidated(entity.getActivationToken() == null); + bean.setPgpPublicKey(entity.getPgpPublicKey()); return bean; } @@ -579,4 +580,37 @@ public class PollenUserService extends PollenServiceSupport implements PollenSer } return result; } + + public PollenUserEmailAddressBean editEmailAddress(PollenUserEmailAddressBean emailAddress) throws PollenDefaultEmailAddressException { + PollenUser connectedUser = checkAndGetConnectedUser(); + return editEmailAddressFromUser(connectedUser, emailAddress); + } + + public PollenUserEmailAddressBean editEmailAddress(String userId, PollenUserEmailAddressBean emailAddress) throws PollenDefaultEmailAddressException { + checkConnectedUserOrAdmin(userId); + return editEmailAddressFromUser(getUser0(userId), emailAddress); + } + + + protected PollenUserEmailAddressBean editEmailAddressFromUser(PollenUser user, PollenUserEmailAddressBean emailAddress) { + checkIsConnectedRequired(); + + checkNotNull(emailAddress); + checkIsPersisted(emailAddress); + + PollenUserEmailAddress result = getPollenUserEmailAddressDao().forTopiaIdEquals(emailAddress.getEntityId()).findUnique(); + + if (!user.containsEmailAddresses(result)) { + + throw new InvalidEntityLinkException(PollenUser.PROPERTY_EMAIL_ADDRESSES, user, result); + + } + + result.setPgpPublicKey(emailAddress.getPgpPublicKey()); + + commit(); + + return toPollenUserEmailAddressBean(result); + + } } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java index 1b9f71d0..541cbb4a 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java @@ -28,6 +28,7 @@ import com.github.mustachejava.MustacheFactory; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.IterableUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; @@ -508,7 +509,15 @@ public class EmailService extends PollenServiceSupport { mustache.execute(stringWriter, mail); - return stringWriter.toString() + mail.getSigning(); + String body = stringWriter.toString() + mail.getSigning(); + + if (mail.getTos().size() == 1) { + + body = getCryptoService().encryptMailIfAsKey(body, IterableUtils.get(mail.getTos(), 0)); + + } + + return body; } diff --git a/pollen-ui-riot-js/src/main/web/i18n/fr.json b/pollen-ui-riot-js/src/main/web/i18n/fr.json index 958bdef1..d20ebba6 100644 --- a/pollen-ui-riot-js/src/main/web/i18n/fr.json +++ b/pollen-ui-riot-js/src/main/web/i18n/fr.json @@ -649,5 +649,16 @@ "emailAddressList_deleteEmailAddress": "Supprimer l'adresse électronique", "emailAddressList_deleteEmailAddressMessage": "Supprimer l'adresse électronique {0} ?", "emailAddressList_newAddressPlaceholder": "Entrez une nouvelle adresse électronique", - "emailAddressList_addEmailAddress": "Ajouter une adresse électronique" + "emailAddressList_addEmailAddress": "Ajouter une adresse électronique", + "emailAddressList_addPgpPublicKey": "Ajouter une clé de chiffrement", + "emailAddressList_editPgpPublicKey": "Modifier la clé de chiffrement", + "emailAddressList_pgpPublicKey": "Clé public PGP", + "emailAddressList_pgpPublicKeyPlaceHolder": "-----BEGIN PGP PUBLIC KEY BLOCK-----", + "emailAddressList_cancelPgpPublicKey": "Annuler", + "emailAddressList_removePgpPublicKey": "Supprimer la clé", + "emailAddressList_removePgpPublicKey_ask": "Supprimer la clé public PGP ?", + "emailAddressList_savePgpPublicKey": "Enregistrer la clé", + "emailAddressList_sendPgpPublicKey": "Envoyer la clé", + "emailAddressList_updatedPgpPublicKey": "La clé public PGP est enregistrée avec succès.", + "emailAddressList_removedPgpPublicKey": "La clé public PGP est supprimée avec succès." } diff --git a/pollen-ui-riot-js/src/main/web/js/UserService.js b/pollen-ui-riot-js/src/main/web/js/UserService.js index 10b9f78e..44578082 100644 --- a/pollen-ui-riot-js/src/main/web/js/UserService.js +++ b/pollen-ui-riot-js/src/main/web/js/UserService.js @@ -139,6 +139,17 @@ class UserService extends FetchService { } return this.doDelete(url + "/avatar"); } + + saveEmailAdress(emailAddress, userId) { + let url; + if (userId) { + url = this._getUsersUrlPrefix(userId); + } else { + url = this._getUserUrlPrefix(); + } + url += "/email/" + emailAddress.id; + return this.post(url, emailAddress); + } } export default singleton(UserService); diff --git a/pollen-ui-riot-js/src/main/web/tag/UserProfile.tag.html b/pollen-ui-riot-js/src/main/web/tag/UserProfile.tag.html index f3be0161..e30bf2fe 100644 --- a/pollen-ui-riot-js/src/main/web/tag/UserProfile.tag.html +++ b/pollen-ui-riot-js/src/main/web/tag/UserProfile.tag.html @@ -399,14 +399,6 @@ import "./components/Avatar.tag.html"; margin-top: 20px; } - .email-address { - display: flex; - justify-content: space-around; - align-items: center; - margin: 1px 0; - padding: 1px 5px; - } - .email-address.odd { background-color: var(--list-alternate-row); } diff --git a/pollen-ui-riot-js/src/main/web/tag/components/UserEmailAddressList.tag.html b/pollen-ui-riot-js/src/main/web/tag/components/UserEmailAddressList.tag.html index 03a9adc1..8d14a457 100644 --- a/pollen-ui-riot-js/src/main/web/tag/components/UserEmailAddressList.tag.html +++ b/pollen-ui-riot-js/src/main/web/tag/components/UserEmailAddressList.tag.html @@ -20,37 +20,84 @@ --> <UserEmailAddressList> <h3 class="c-heading"><i class="fa fa-at"/> {_t.emailAddresses}</h3> - <div each="{emailAddress, index in opts.user.emailAddresses}" class="email-address {index % 2 == 0 ? 'even' : 'odd'}"> - <span class="email-address-label {validation-pending : !emailAddress.validated} {default-address : parent.opts.user.defaultEmailAddress.id === emailAddress.id}"> - {emailAddress.emailAddress} - </span> - <button type="button" - class="c-button u-small" - if={!emailAddress.validated} - title="{parent._t.resendValidation}" - onclick={parent.resendValidation(emailAddress.emailAddress)}> - <i class="fa fa-paper-plane" aria-hidden="true"></i> - </button> - <button type="button" - class="c-button u-small c-button--success" - if={!emailAddress.validated && parent.opts.admin} - title="{parent._t.validate}" - onclick={validate(emailAddress.id, index)}> - <i class="fa fa-check" aria-hidden="true"></i> - </button> - <button if="{parent.opts.user.defaultEmailAddress.id !== emailAddress.id && emailAddress.validated}" - type="button" class="c-button u-small c-button--info" - title="{parent._t.defaultEmailAddress}" - onclick="{parent.setDefaultEmailAddress(emailAddress.id, index)}"> - <i class="fa fa-envelope"></i> - </button> - <button disabled="{parent.opts.user.defaultEmailAddress.id === emailAddress.id}" - type="button" class="c-button u-small c-button--error" - title="{parent._t.deleteEmailAddress}" - onclick="{parent.deleteEmailAddress(emailAddress.id, index)}"> - <i class="fa fa-trash"></i> - </button> - </div> + <ul> + <li each="{emailAddress, index in opts.user.emailAddresses}" class="email-address {index % 2 == 0 ? 'even' : 'odd'}"> + <div class="email-address-header"> + <span class="email-address-label {validation-pending : !emailAddress.validated} {default-address : parent.opts.user.defaultEmailAddress.id === emailAddress.id}"> + {emailAddress.emailAddress} + </span> + <button type="button" + class="c-button u-small" + if={!emailAddress.validated} + title="{parent._t.resendValidation}" + onclick={parent.resendValidation(emailAddress.emailAddress)}> + <i class="fa fa-paper-plane" aria-hidden="true"></i> + </button> + <button type="button" + class="c-button u-small c-button--success" + if={!emailAddress.validated && parent.opts.admin} + title="{parent._t.validate}" + onclick={validate(emailAddress.id, index)}> + <i class="fa fa-check" aria-hidden="true"></i> + </button> + <button if="{parent.opts.user.defaultEmailAddress.id !== emailAddress.id && emailAddress.validated}" + type="button" class="c-button u-small c-button--info" + title="{parent._t.defaultEmailAddress}" + onclick="{parent.setDefaultEmailAddress(emailAddress.id, index)}"> + <i class="fa fa-envelope"></i> + </button> + <button type="button" + class="c-button u-small c-button--warning" + if={emailAddress.validated} + title="{emailAddress.pgpPublicKey ? parent._t.editPgpPublicKey : parent._t.addPgpPublicKey}" + onclick={parent.openPgpPublicKey(emailAddress)}> + <i class="fa {fa-lock: emailAddress.pgpPublicKey, fa-unlock: !emailAddress.pgpPublicKey}" aria-hidden="true"></i> + </button> + <button disabled="{parent.opts.user.defaultEmailAddress.id === emailAddress.id}" + type="button" class="c-button u-small c-button--error" + title="{parent._t.deleteEmailAddress}" + onclick="{parent.deleteEmailAddress(emailAddress.id, index)}"> + <i class="fa fa-trash"></i> + </button> + </div> + <form class="identity-form" show={emailAddress.showKey}> + <HumanInput onsubmit={savePgpPublicKey(emailAddress)}/> + <div class="o-form-element"> + <label class="c-label" for="pgpPublicKey">{parent._t.pgpPublicKey}</label> + <textarea ref="pgpPublicKey-{emailAddress.id}" + class="c-field c-field--label" + name="pgpPublicKey" + required + placeholder="{parent._t.pgpPublicKeyPlaceHolder}">{emailAddress.pgpPublicKey}</textarea> + </div> + <div class="actions"> + <div class="actions-left"> + <button type="button" + onclick={parent.cancelPgpPublicKey(emailAddress)} + class="c-button c-button--ghost-info"> + <i class="fa fa-undo" aria-hidden="true"></i> + {parent._t.cancelPgpPublicKey} + </button> + </div> + + <div class="actions-right"> + <button type="button" + if={emailAddress.pgpPublicKey} + onclick={parent.removePgpPublicKey(emailAddress)} + class="c-button c-button--error"> + <i class="fa fa-times" aria-hidden="true"></i> + {parent._t.removePgpPublicKey} + </button> + <button type="submit" + class="c-button c-button--info"> + <i class="fa fa-check" aria-hidden="true"></i> + {emailAddress.pgpPublicKey ? parent._t.savePgpPublicKey : parent._t.sendPgpPublicKey} + </button> + </div> + </div> + </div> + </li> + </ul> <div ref="new-email-form" class="new-email-form"> <div class="o-form-element c-input-group"> <div class="o-field o-field--icon-right"> @@ -77,7 +124,7 @@ import Message from "../../js/Message"; import userService from "../../js/UserService"; import authService from "../../js/AuthService"; - + this.installBundle(session, "emailAddressList"); this.errors = {}; @@ -114,6 +161,56 @@ }); }; + this.setDefaultEmailAddress = (emailAddressId, index) => () => { + userService.setDefaultEmailAddress(emailAddressId, this.opts.admin ? this.opts.user.id : null).then(() => { + this.opts.user.defaultEmailAddress = this.opts.user.emailAddresses[index]; + this.update(); + }); + }; + + this.openPgpPublicKey = emailAddress => () => { + this.opts.user.emailAddresses.forEach(mail => { + mail.showKey = emailAddress === mail; + }); + }; + + this.cancelPgpPublicKey = emailAddress => () => { + this.refs["pgpPublicKey-" + emailAddress.id].value = emailAddress.pgpPublicKey; + emailAddress.showKey = false; + }; + + this.removePgpPublicKey = emailAddress => () => { + this.confirm(this._t.removePgpPublicKey_ask).then((confirm) => { + if (confirm) { + this.savePgpPublicKey2(emailAddress, null).then(() => { + this.bus.trigger("message", this._t.removedPgpPublicKey, "success"); + this.update(); + }); + } + }); + }; + + this.savePgpPublicKey = emailAddress => () => { + this.savePgpPublicKey2(emailAddress, this.refs["pgpPublicKey-" + emailAddress.id].value).then(() => { + this.bus.trigger("message", this._t.updatedPgpPublicKey, "success"); + this.update(); + }); + }; + + this.savePgpPublicKey2 = (emailAddress, pgpPublicKey) => { + let emailAddress2 = { + id: emailAddress.id, + emailAddress: emailAddress.emailAddress, + validated: emailAddress.validated, + pgpPublicKey: pgpPublicKey + }; + return userService.saveEmailAdress(emailAddress2, this.opts.admin ? this.opts.user.id : null).then((email) => { + emailAddress.showKey = false; + emailAddress.pgpPublicKey = email.pgpPublicKey; + return emailAddress; + }); + }; + this.submitEmailAddress = () => { let emailAddress = this.refs.newEmailAddress.value; userService.addEmailAddressToUser(emailAddress, this.opts.admin ? this.opts.user.id : null).then((result) => { @@ -131,11 +228,14 @@ <style> .email-address { + margin: 1px 0; + padding: 1px 5px; + } + + .email-address-header { display: flex; justify-content: space-around; align-items: center; - margin: 1px 0; - padding: 1px 5px; } .email-address.odd { @@ -147,6 +247,10 @@ padding: 0 5px; } + .email-address .actions { + justify-content: space-between; + } + .email-address-label.default-address { font-weight: bold; } diff --git a/pom.xml b/pom.xml index 47b6aedb..f2cec315 100644 --- a/pom.xml +++ b/pom.xml @@ -195,6 +195,7 @@ <tomcatEmbedLoggingVersion>8.5.2</tomcatEmbedLoggingVersion> <hibernateVersion>5.2.10.Final</hibernateVersion> <httpCommonsHttpclientVersion>4.5.5</httpCommonsHttpclientVersion> + <bouncycastleVersion>1.59</bouncycastleVersion> <pollenI18nBundle>pollen-i18n</pollenI18nBundle> <!-- license to use --> @@ -478,6 +479,18 @@ <scope>runtime</scope> </dependency> + <!--Crypto--> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + <version>${bouncycastleVersion}</version> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpg-jdk15on</artifactId> + <version>${bouncycastleVersion}</version> + </dependency> + <!-- Others --> <dependency> -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.