From ff358945f2b210f7f374d3e3e7e36170ee16aed5 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Tue, 3 Nov 2020 06:16:17 +0100 Subject: KB68 Newsletter registration and deregistration. Change-Id: Ic79fe64f03ce887879bffc7623e59cb697065ee6 --- .../mulk/mulkcms2/benki/newsletter/MailRouter.java | 32 +++++++++ .../benki/newsletter/NewsletterResource.java | 76 ++++++++++++++++++++++ .../benki/newsletter/NewsletterSender.java | 2 +- .../benki/newsletter/NewsletterSubscription.java | 17 ++++- .../benki/newsletter/NewsletterUnsubscriber.java | 63 ++++++++++++++++++ src/main/resources/application.properties | 6 ++ src/main/resources/db/changeLog-1.7.xml | 14 ++++ .../NewsletterResource/completeRegistration.html | 24 +++++++ .../templates/NewsletterResource/index.html | 32 +++++++++ .../templates/NewsletterResource/registered.html | 26 ++++++++ .../NewsletterResource/registrationMail.txt | 12 ++++ .../NewsletterUnsubscriber/unsubscribedMail.txt | 7 ++ src/main/resources/templates/tags/navbar.html | 2 + 13 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 src/main/java/eu/mulk/mulkcms2/benki/newsletter/MailRouter.java create mode 100644 src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterResource.java create mode 100644 src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterUnsubscriber.java create mode 100644 src/main/resources/templates/NewsletterResource/completeRegistration.html create mode 100644 src/main/resources/templates/NewsletterResource/index.html create mode 100644 src/main/resources/templates/NewsletterResource/registered.html create mode 100644 src/main/resources/templates/NewsletterResource/registrationMail.txt create mode 100644 src/main/resources/templates/NewsletterUnsubscriber/unsubscribedMail.txt (limited to 'src/main') diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/MailRouter.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/MailRouter.java new file mode 100644 index 0000000..1d6ea5c --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/MailRouter.java @@ -0,0 +1,32 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import javax.enterprise.context.Dependent; +import javax.inject.Inject; +import org.apache.camel.builder.RouteBuilder; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@Dependent +public class MailRouter extends RouteBuilder { + + @ConfigProperty(name = "quarkus.mailer.host") + String emailHost; + + @ConfigProperty(name = "mulkcms.imap.port") + int emailPort; + + @ConfigProperty(name = "quarkus.mailer.username") + String emailUser; + + @ConfigProperty(name = "quarkus.mailer.password") + String emailPassword; + + @Inject NewsletterUnsubscriber newsletterUnsubscriber; + + @Override + public void configure() { + fromF( + "imaps://%s:%d?password=%s&username=%s&searchTerm.to=unsubscribe", + emailHost, emailPort, emailPassword, emailUser) + .process(newsletterUnsubscriber); + } +} diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterResource.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterResource.java new file mode 100644 index 0000000..a46ee32 --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterResource.java @@ -0,0 +1,76 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import static javax.ws.rs.core.MediaType.TEXT_HTML; + +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import javax.transaction.Transactional; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response.Status; + +@Path("/newsletter") +@Produces(TEXT_HTML) +public class NewsletterResource { + + @CheckedTemplate + static class Templates { + public static native MailTemplateInstance registrationMail(String registrationKey); + + public static native TemplateInstance index(); + + public static native TemplateInstance completeRegistration(); + + public static native TemplateInstance registered(); + } + + @GET + public TemplateInstance getIndex() { + return Templates.index(); + } + + @POST + @Path("register") + @Transactional + public CompletionStage register(@FormParam("email") String email) { + var existingSubscription = + NewsletterSubscription.find("email = ?1", email) + .singleResultOptional(); + if (existingSubscription.isPresent()) { + // If a subscription already exists, act as if we had created it. This provides better + // privacy to users than an error message does. + return CompletableFuture.completedStage(Templates.completeRegistration()); + } + + var subscription = new NewsletterSubscription(); + subscription.email = email; + subscription.persist(); + + var mailText = Templates.registrationMail(subscription.registrationKey); + var sendJob = mailText.subject("MulkCMS newsletter registration").to(email).send(); + return sendJob.thenApply((x) -> Templates.completeRegistration()); + } + + @GET + @Path("finish-registration") + @Transactional + public TemplateInstance finishRegistration(@QueryParam("key") String registrationKey) { + NewsletterSubscription.find("registrationKey = ?1", registrationKey) + .singleResultOptional() + .ifPresentOrElse( + s -> s.registrationKey = null, + () -> { + throw new ClientErrorException(Status.BAD_REQUEST); + }); + + return Templates.registered(); + } +} diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java index 1f13c08..59ab37c 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java @@ -84,7 +84,7 @@ public class NewsletterSender { posts.forEach(post -> post.newsletter = newsletter); var subscriberEmails = - NewsletterSubscription.streamAll() + NewsletterSubscription.stream("registrationKey IS NULL") .map(x -> x.email) .toArray(String[]::new); diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java index 7aeda60..cd50b2e 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java @@ -1,10 +1,13 @@ package eu.mulk.mulkcms2.benki.newsletter; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import java.math.BigInteger; +import java.security.SecureRandom; import java.time.OffsetDateTime; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.validation.constraints.Email; @@ -14,9 +17,11 @@ import org.hibernate.annotations.NaturalId; @Table(name = "newsletter_subscriptions", schema = "benki") public class NewsletterSubscription extends PanacheEntityBase { + private static final int registrationKeyBytes = 32; + @Id @Column(name = "id", nullable = false) - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) public Integer id; @Column(name = "start_date", nullable = false) @@ -26,4 +31,14 @@ public class NewsletterSubscription extends PanacheEntityBase { @Column(name = "email", nullable = false) @Email public String email; + + @Column(name = "registration_key", nullable = true) + public String registrationKey = generateRegistrationKey(); + + private static String generateRegistrationKey() { + var secureRandom = new SecureRandom(); + byte[] keyBytes = new byte[registrationKeyBytes]; + secureRandom.nextBytes(keyBytes); + return new BigInteger(keyBytes).abs().toString(36); + } } diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterUnsubscriber.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterUnsubscriber.java new file mode 100644 index 0000000..0a67ff6 --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterUnsubscriber.java @@ -0,0 +1,63 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.qute.api.CheckedTemplate; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.enterprise.context.Dependent; +import javax.mail.internet.InternetAddress; +import javax.transaction.Transactional; +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.apache.camel.component.mail.MailMessage; +import org.jboss.logging.Logger; + +@Dependent +public class NewsletterUnsubscriber implements Processor { + + private static final Logger log = Logger.getLogger(NewsletterUnsubscriber.class); + + @CheckedTemplate + static class Templates { + public static native MailTemplateInstance unsubscribedMail(); + } + + @Override + @Transactional + public void process(Exchange exchange) throws Exception { + var message = exchange.getMessage(MailMessage.class); + var mail = message.getMessage(); + + for (var sender : mail.getFrom()) { + if (!(sender instanceof InternetAddress)) { + log.warnf("Tried to unsubscribe, but not an InternetAddress: %s", sender); + continue; + } + + var address = ((InternetAddress) sender).getAddress(); + var subscription = + NewsletterSubscription.find("email = ?1", address) + .singleResultOptional(); + subscription.ifPresentOrElse( + s -> { + try { + var sendJob = + Templates.unsubscribedMail() + .subject("Unsubscribed from MulkCMS newsletter") + .to(address) + .send(); + sendJob.toCompletableFuture().get(60, TimeUnit.SECONDS); + + s.delete(); + + log.infof("Unsubscribed: %s (#%d)", s.email, s.id); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + }, + () -> + log.warnf("Tried to unsubscribe, but no subscription found: %s", sender.toString())); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b7c54da..088e4e0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -68,6 +68,12 @@ quarkus.mailer.host = mail.benkard.de quarkus.mailer.port = 587 quarkus.mailer.start-tls = REQUIRED quarkus.mailer.username = mulkcms@benkard.de +mulkcms.imap.port = 993 + +%dev.quarkus.mailer.host = mail.benkard.de +%dev.quarkus.mailer.from = test@benkard.de +%dev.quarkus.mailer.username = test@benkard.de +%dev.quarkus.mailer.password = test # Deployment docker.registry = docker.benkard.de diff --git a/src/main/resources/db/changeLog-1.7.xml b/src/main/resources/db/changeLog-1.7.xml index 8824115..cfdad49 100644 --- a/src/main/resources/db/changeLog-1.7.xml +++ b/src/main/resources/db/changeLog-1.7.xml @@ -52,4 +52,18 @@ + + + + + + + + + + + + diff --git a/src/main/resources/templates/NewsletterResource/completeRegistration.html b/src/main/resources/templates/NewsletterResource/completeRegistration.html new file mode 100644 index 0000000..0cdb874 --- /dev/null +++ b/src/main/resources/templates/NewsletterResource/completeRegistration.html @@ -0,0 +1,24 @@ +{#include base.html} + +{#title}Newsletter Registration — Benki{/title} +{#siteSection}Newsletter{/siteSection} +{#wikiClass}this-page{/wikiClass} + +{#nav}{#navbar siteSection="Newsletter" /} + +{#head}{/head} + +{#body} +
+
+

Newsletter Registration

+
+ +
+

In order to complete your registration, please check your email inbox and click + the link in the confirmation mail.

+
+
+{/body} + +{/include} diff --git a/src/main/resources/templates/NewsletterResource/index.html b/src/main/resources/templates/NewsletterResource/index.html new file mode 100644 index 0000000..84098bd --- /dev/null +++ b/src/main/resources/templates/NewsletterResource/index.html @@ -0,0 +1,32 @@ +{#include base.html} + +{#title}Newsletter — Benki{/title} +{#siteSection}Newsletter{/siteSection} +{#wikiClass}this-page{/wikiClass} + +{#nav}{#navbar siteSection="Newsletter" /} + +{#head}{/head} + +{#body} +
+
+

Newsletter

+
+ +
+

To get a weekly digest of all posted articles and bookmarks into your email inbox, + enter your email address and submit the form below.

+ +

Your registration is subject to the privacy policy.

+ +
+ + + +
+
+
+{/body} + +{/include} diff --git a/src/main/resources/templates/NewsletterResource/registered.html b/src/main/resources/templates/NewsletterResource/registered.html new file mode 100644 index 0000000..e82618e --- /dev/null +++ b/src/main/resources/templates/NewsletterResource/registered.html @@ -0,0 +1,26 @@ +{#include base.html} + +{#title}Newsletter Registration — Benki{/title} +{#siteSection}Newsletter{/siteSection} +{#wikiClass}this-page{/wikiClass} + +{#nav}{#navbar siteSection="Newsletter" /} + +{#head}{/head} + +{#body} +
+
+

Newsletter Registration

+
+ +
+

Thank you. You are now subscribed to the weekly email digest.

+ +

To unsubscribe, send an email to + mulkcms+unsubscribe@benkard.de.

+
+
+{/body} + +{/include} diff --git a/src/main/resources/templates/NewsletterResource/registrationMail.txt b/src/main/resources/templates/NewsletterResource/registrationMail.txt new file mode 100644 index 0000000..c86b385 --- /dev/null +++ b/src/main/resources/templates/NewsletterResource/registrationMail.txt @@ -0,0 +1,12 @@ +{@java.lang.String registrationKey} +Hello! + +Someone (you, one would hope) entered your email address to subscribe to the MulkCMS +benkard.de newsletter. In order to complete your registration, open the following +link in your web browser: + + https://matthias.benkard.de/newsletter/finish-registration?key={registrationKey} + +If someone is playing a trick on you and you would not actually like to subscribe to +the newsletter, you may ignore this email. In this case, your registration record +will be deleted within a week. \ No newline at end of file diff --git a/src/main/resources/templates/NewsletterUnsubscriber/unsubscribedMail.txt b/src/main/resources/templates/NewsletterUnsubscriber/unsubscribedMail.txt new file mode 100644 index 0000000..56f8d0e --- /dev/null +++ b/src/main/resources/templates/NewsletterUnsubscriber/unsubscribedMail.txt @@ -0,0 +1,7 @@ +You have been unsubscribed from the MulkCMS newsletter because we +received an unsubscription request from your email address. + +To register for the newsletter again, open the following page in +your web browser: + + https://matthias.benkard.de/newsletter diff --git a/src/main/resources/templates/tags/navbar.html b/src/main/resources/templates/tags/navbar.html index 63f88f1..d79a952 100644 --- a/src/main/resources/templates/tags/navbar.html +++ b/src/main/resources/templates/tags/navbar.html @@ -9,6 +9,8 @@
  • Wiki
  • {/if} +
  • Newsletter
  • +
  • Contact Info
  • Privacy Policy
  • -- cgit v1.2.3