summaryrefslogtreecommitdiff
path: root/src/main/java/eu
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/eu')
-rw-r--r--src/main/java/eu/mulk/mulkcms2/benki/newsletter/MailRouter.java32
-rw-r--r--src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterResource.java76
-rw-r--r--src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java2
-rw-r--r--src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java17
-rw-r--r--src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterUnsubscriber.java63
5 files changed, 188 insertions, 2 deletions
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<TemplateInstance> register(@FormParam("email") String email) {
+ var existingSubscription =
+ NewsletterSubscription.<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.<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.<NewsletterSubscription>streamAll()
+ NewsletterSubscription.<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.<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()));
+ }
+ }
+}