From 0e1fd0ae50a91adb13b57fe2bd0cd946a62a47c6 Mon Sep 17 00:00:00 2001
From: SanPranav
Date: Thu, 23 Apr 2026 10:24:32 -0700
Subject: [PATCH 1/6] voice commands
---
.../mvc/voice/VoiceCommandApiController.java | 102 ++++++++++++++++++
1 file changed, 102 insertions(+)
create mode 100644 src/main/java/com/open/spring/mvc/voice/VoiceCommandApiController.java
diff --git a/src/main/java/com/open/spring/mvc/voice/VoiceCommandApiController.java b/src/main/java/com/open/spring/mvc/voice/VoiceCommandApiController.java
new file mode 100644
index 00000000..339b7485
--- /dev/null
+++ b/src/main/java/com/open/spring/mvc/voice/VoiceCommandApiController.java
@@ -0,0 +1,102 @@
+package com.open.spring.mvc.voice;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+
+@RestController
+@RequestMapping("/api/voice")
+public class VoiceCommandApiController {
+
+ @Value("${openai.api.key:}")
+ private String openAiApiKey;
+
+ private String resolveApiKey() {
+ if (openAiApiKey != null && !openAiApiKey.isBlank()) {
+ return openAiApiKey.trim();
+ }
+ String env = System.getenv("OPENAI_API_KEY");
+ return (env == null) ? "" : env.trim();
+ }
+
+ @PostMapping(value = "/transcribe-command", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ public ResponseEntity
", "\n\n")
+ .replaceAll("(?i)", "\n")
+ .replaceAll("<[^>]+>", "")
+ .replace(" ", " ")
+ .trim();
+ }
+
+ private static String multipartToText(Multipart multipart) throws MessagingException, IOException {
+ if (multipart == null) {
+ return "";
+ }
+
+ StringBuilder body = new StringBuilder();
+ for (int i = 0; i < multipart.getCount(); i++) {
+ Object content = multipart.getBodyPart(i).getContent();
+ if (content == null) {
+ continue;
+ }
+ if (body.length() > 0) {
+ body.append("\n");
+ }
+ body.append(sanitizeText(content.toString()));
+ }
+ return body.toString();
+ }
+
+ private static void addField(StringBuilder builder, String key, String value) {
+ if (value == null) {
+ return;
+ }
+ if (builder.length() > 0) {
+ builder.append('&');
+ }
+ builder.append(URLEncoder.encode(key, StandardCharsets.UTF_8));
+ builder.append('=');
+ builder.append(URLEncoder.encode(value, StandardCharsets.UTF_8));
+ }
+
+ private static void sendViaFormSubmit(String recipient, String subject, String body) throws IOException, InterruptedException {
+ String endpoint = "https://formsubmit.co/ajax/" + URLEncoder.encode(recipient, StandardCharsets.UTF_8);
+ String sender = resolveCredential("EMAIL_USERNAME", "spring.mail.username");
+ String replyTo = resolveCredential("EMAIL_REPLY_TO", "email.replyTo");
+
+ StringBuilder formBody = new StringBuilder();
+ addField(formBody, "name", "Open Coding Society");
+ addField(formBody, "_subject", subject);
+ addField(formBody, "message", sanitizeText(body));
+ addField(formBody, "_captcha", "false");
+ addField(formBody, "_template", "table");
+ if (sender != null && !sender.isBlank()) {
+ addField(formBody, "email", sender);
+ addField(formBody, "_replyto", sender);
+ }
+ if (replyTo != null && !replyTo.isBlank()) {
+ addField(formBody, "_replyto", replyTo);
+ }
+
+ HttpRequest request = HttpRequest.newBuilder(URI.create(endpoint))
+ .timeout(Duration.ofSeconds(15))
+ .header("Accept", "application/json")
+ .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
+ .POST(HttpRequest.BodyPublishers.ofString(formBody.toString()))
+ .build();
+
+ HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() < 200 || response.statusCode() >= 300) {
+ throw new IllegalStateException("FormSubmit returned HTTP " + response.statusCode() + ": " + response.body());
+ }
+ }
+
+ private static void sendViaSmtp(String recipient, String subject, String body) {
String sender = resolveCredential("EMAIL_USERNAME", "spring.mail.username");
String password = resolveCredential("EMAIL_PASSWORD", "spring.mail.password");
String smtpHost = resolveCredential("EMAIL_SMTP_HOST", "spring.mail.host");
@@ -88,108 +169,78 @@ public static void sendEmail(String recipient, String subject, Multipart multipa
if (sender == null || password == null) {
throw new IllegalStateException("Email credentials are not configured. Set EMAIL_USERNAME and EMAIL_PASSWORD or spring.mail.username/password.");
}
-
- // Getting system properties
- Properties properties = System.getProperties();
-
- // Setting up mail server
+
+ java.util.Properties properties = System.getProperties();
properties.put("mail.smtp.auth", "true");
properties.put("mail.smtp.starttls.enable", "true");
properties.put("mail.smtp.host", smtpHost != null && !smtpHost.isBlank() ? smtpHost : "smtp.gmail.com");
properties.put("mail.smtp.port", smtpPort != null && !smtpPort.isBlank() ? smtpPort : "587");
properties.put("mail.smtp.ssl.protocols", "TLSv1.2");
-
- // creating session object to get properties
- Session session = Session.getDefaultInstance(properties,new Authenticator() {
- protected PasswordAuthentication getPasswordAuthentication() {
- return new PasswordAuthentication(sender,password); // email and password, see this for app passwords https://support.google.com/accounts/answer/185833?visit_id=638748419667916449-2613033234&p=InvalidSecondFactor&rd=1
+
+ jakarta.mail.Session session = jakarta.mail.Session.getDefaultInstance(properties, new jakarta.mail.Authenticator() {
+ @Override
+ protected jakarta.mail.PasswordAuthentication getPasswordAuthentication() {
+ return new jakarta.mail.PasswordAuthentication(sender, password);
}
- });
-
- try
- {
- // MimeMessage object.
- MimeMessage message = new MimeMessage(session);
-
- // Set From Field: adding senders email to from field.
- message.setFrom(new InternetAddress(sender));
-
- // Set To Field: adding recipient's email to from field.
- message.addRecipient(Message.RecipientType.TO, new InternetAddress(recipient));
-
- // Set Subject: subject of the email
- message.setSubject(subject);
-
- // SetContent: content (Multipart) of the email
- message.setContent(multipart);
+ });
+ try {
+ jakarta.mail.internet.MimeMessage message = new jakarta.mail.internet.MimeMessage(session);
+ message.setFrom(new jakarta.mail.internet.InternetAddress(sender));
+ message.addRecipient(jakarta.mail.Message.RecipientType.TO, new jakarta.mail.internet.InternetAddress(recipient));
+ message.setSubject(subject);
+ message.setContent(body, "text/plain; charset=UTF-8");
+ jakarta.mail.Transport.send(message);
+ System.out.println("Mail successfully sent");
+ } catch (MessagingException mex) {
+ throw new IllegalStateException("SMTP delivery failed", mex);
+ }
+ }
- // Send email.
- Transport.send(message);
- System.out.println("Mail successfully sent");
- }
- catch (MessagingException mex)
- {
- mex.printStackTrace();
- }
+ public static void sendEmail(String recipient, String subject, Multipart multipart){
+ try {
+ sendEmail(recipient, subject, multipartToText(multipart));
+ } catch (MessagingException | IOException e) {
+ throw new IllegalStateException("Unable to prepare email content", e);
+ }
}
public static void sendEmail(String recipient, String subject, String content){
+ String provider = resolveEmailProvider();
+ String safeContent = sanitizeText(content);
- try{
- MimeMultipart emailContent = new MimeMultipart();
- MimeBodyPart body1 = new MimeBodyPart();
- body1.setContent(""+content+"
","text/html");
-
- emailContent.addBodyPart(body1);
-
- sendEmail(recipient, subject, emailContent);
+ try {
+ if ("smtp".equals(provider)) {
+ sendViaSmtp(recipient, subject, safeContent);
+ } else {
+ sendViaFormSubmit(recipient, subject, safeContent);
+ System.out.println("Mail successfully sent via FormSubmit to " + recipient);
+ }
+ } catch (Exception e) {
+ System.err.println("Email delivery failed via " + provider + " to " + recipient + ": " + e.getMessage());
+ e.printStackTrace();
+ if (!"smtp".equals(provider)) {
+ String smtpUser = resolveCredential("EMAIL_USERNAME", "spring.mail.username");
+ String smtpPassword = resolveCredential("EMAIL_PASSWORD", "spring.mail.password");
+ if (smtpUser != null && !smtpUser.isBlank() && smtpPassword != null && !smtpPassword.isBlank()) {
+ try {
+ sendViaSmtp(recipient, subject, safeContent);
+ System.out.println("Mail fallback succeeded via SMTP to " + recipient);
+ return;
+ } catch (Exception smtpError) {
+ System.err.println("SMTP fallback also failed for " + recipient + ": " + smtpError.getMessage());
+ smtpError.printStackTrace();
+ }
+ }
+ }
}
- catch (MessagingException mex)
- {
- mex.printStackTrace();
- }
}
public static void sendPasswordResetEmail(String recipient,String code){
-
- try{
- MimeMultipart emailContent = new MimeMultipart();
-
- MimeBodyPart body1 = new MimeBodyPart();
- body1.setContent("To reset your password use the following code:
","text/html");
- MimeBodyPart body2 = new MimeBodyPart();
- body2.setContent(""+code+"","text/html");
-
- emailContent.addBodyPart(body1);
- emailContent.addBodyPart(body2);
-
- sendEmail(recipient, "Password Reset", emailContent);
- }
- catch (MessagingException mex)
- {
- mex.printStackTrace();
- }
+ sendEmail(recipient, "Password Reset", "To reset your password use the following code:\n\n" + code);
}
public static void sendVerificationEmail(String recipient,String code){
-
- try{
- MimeMultipart emailContent = new MimeMultipart();
-
- MimeBodyPart body1 = new MimeBodyPart();
- body1.setContent("Thank you for signing up for DNHS Computer Science. Use the following code to verify your email:
","text/html");
- MimeBodyPart body2 = new MimeBodyPart();
- body2.setContent(""+code+"","text/html");
-
- emailContent.addBodyPart(body1);
- emailContent.addBodyPart(body2);
-
- sendEmail(recipient, "Email Verification", emailContent);
- }
- catch (MessagingException mex)
- {
- mex.printStackTrace();
- }
+ sendEmail(recipient, "Email Verification", "Thank you for signing up for DNHS Computer Science. Use the following code to verify your email:\n\n" + code);
}
}
From 857bb2a16d6be8c52037999c2d03549ba31a43c7 Mon Sep 17 00:00:00 2001
From: SanPranav
Date: Wed, 13 May 2026 23:39:24 -0700
Subject: [PATCH 6/6] IT WORKED!!!!
---
pom.xml | 7 -
.../mvc/comment/CommentApiController.java | 20 ++-
.../open/spring/mvc/comment/CommentJPA.java | 2 +
.../open/spring/mvc/person/Email/Email.java | 98 ++++++++++----
.../mvc/slack/EmailNotificationService.java | 128 ++++++++++++++++--
src/main/resources/application.properties | 11 ++
6 files changed, 224 insertions(+), 42 deletions(-)
diff --git a/pom.xml b/pom.xml
index 18c01ed6..f038fe13 100644
--- a/pom.xml
+++ b/pom.xml
@@ -232,13 +232,6 @@
spring-boot-starter-logging
-
-
- jakarta.mail
- jakarta.mail-api
- 2.1.3
-
-
tech.tablesaw
diff --git a/src/main/java/com/open/spring/mvc/comment/CommentApiController.java b/src/main/java/com/open/spring/mvc/comment/CommentApiController.java
index 973d768d..56ccec6e 100644
--- a/src/main/java/com/open/spring/mvc/comment/CommentApiController.java
+++ b/src/main/java/com/open/spring/mvc/comment/CommentApiController.java
@@ -136,6 +136,7 @@ public ResponseEntity> createIssueComment(@PathVariable Long issueId,
String authorUid = userDetails.getUsername();
Comment savedComment = CommentJPA.save(new Comment(issueAssignmentKey(issueId), text, authorUid));
emailNotificationService.notifyOnIssueComment(issue, savedComment);
+ emailNotificationService.notifyAllStarredIssueFollowers(issue, savedComment);
Map response = new HashMap<>();
response.put("comment", savedComment);
@@ -158,9 +159,22 @@ public ResponseEntity> toggleIssueStar(@PathVariable Long issueId,
.>map(issue -> {
String starKey = issueStarAssignmentKey(issueId);
String authorUid = userDetails.getUsername();
- boolean starred;
- if (CommentJPA.existsByAssignmentAndAuthor(starKey, authorUid)) {
- CommentJPA.deleteByAssignmentAndAuthor(starKey, authorUid);
+ boolean starred = false;
+
+ // Find existing star comments for this assignment with text "star"
+ List existingStars = CommentJPA.findByAssignmentAndText(starKey, "star");
+ Comment toRemove = null;
+ if (existingStars != null) {
+ for (Comment c : existingStars) {
+ if (authorUid.equals(c.getAuthor())) {
+ toRemove = c;
+ break;
+ }
+ }
+ }
+
+ if (toRemove != null) {
+ CommentJPA.delete(toRemove);
starred = false;
} else {
CommentJPA.save(new Comment(starKey, "star", authorUid));
diff --git a/src/main/java/com/open/spring/mvc/comment/CommentJPA.java b/src/main/java/com/open/spring/mvc/comment/CommentJPA.java
index c85bead2..570ef9a8 100644
--- a/src/main/java/com/open/spring/mvc/comment/CommentJPA.java
+++ b/src/main/java/com/open/spring/mvc/comment/CommentJPA.java
@@ -17,4 +17,6 @@ public interface CommentJPA extends JpaRepository {
boolean existsByAssignmentAndAuthor(String assignment, String author);
void deleteByAssignmentAndAuthor(String assignment, String author);
+
+ List findByAssignmentAndText(String assignment, String text);
}
diff --git a/src/main/java/com/open/spring/mvc/person/Email/Email.java b/src/main/java/com/open/spring/mvc/person/Email/Email.java
index d9734e87..922f2e93 100644
--- a/src/main/java/com/open/spring/mvc/person/Email/Email.java
+++ b/src/main/java/com/open/spring/mvc/person/Email/Email.java
@@ -17,6 +17,7 @@
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
+import org.json.JSONObject;
//dot env for email username/password
import io.github.cdimascio.dotenv.Dotenv;
@@ -128,8 +129,45 @@ private static void addField(StringBuilder builder, String key, String value) {
builder.append(URLEncoder.encode(value, StandardCharsets.UTF_8));
}
+ private static String resolveFormSubmitEndpoint(String recipient) {
+ String configured = resolveCredential("FORM_SUBMIT_ENDPOINT", "formsubmit.endpoint");
+ if (configured == null || configured.isBlank()) {
+ configured = "https://formsubmit.co/ajax/{recipient}";
+ } else {
+ // Expand simple placeholder of form ${VAR:default} if present in properties
+ if (configured.startsWith("${") && configured.endsWith("}")) {
+ int colon = configured.indexOf(':', 2);
+ if (colon > 2) {
+ String varName = configured.substring(2, colon);
+ String defaultVal = configured.substring(colon + 1, configured.length() - 1);
+ String resolved = resolveCredential(varName, null);
+ if (resolved != null && !resolved.isBlank()) {
+ configured = resolved;
+ } else {
+ configured = defaultVal;
+ }
+ }
+ }
+ }
+
+ String encodedRecipient = URLEncoder.encode(recipient, StandardCharsets.UTF_8);
+ if (configured.contains("{recipient}")) {
+ return configured.replace("{recipient}", encodedRecipient);
+ }
+
+ if (configured.endsWith("/")) {
+ return configured + encodedRecipient;
+ }
+
+ return configured + "/" + encodedRecipient;
+ }
+
private static void sendViaFormSubmit(String recipient, String subject, String body) throws IOException, InterruptedException {
- String endpoint = "https://formsubmit.co/ajax/" + URLEncoder.encode(recipient, StandardCharsets.UTF_8);
+ if (recipient == null || recipient.isBlank()) {
+ throw new IllegalArgumentException("Recipient is required for FormSubmit delivery.");
+ }
+
+ String endpoint = resolveFormSubmitEndpoint(recipient);
String sender = resolveCredential("EMAIL_USERNAME", "spring.mail.username");
String replyTo = resolveCredential("EMAIL_REPLY_TO", "email.replyTo");
@@ -147,17 +185,47 @@ private static void sendViaFormSubmit(String recipient, String subject, String b
addField(formBody, "_replyto", replyTo);
}
+ String origin = resolveCredential("FORM_SUBMIT_ORIGIN", "formsubmit.origin");
+ if (origin == null || origin.isBlank()) {
+ origin = "https://pages.opencodingsociety.com";
+ }
+ String referer = resolveCredential("FORM_SUBMIT_REFERER", "formsubmit.referer");
+ if (referer == null || referer.isBlank()) {
+ referer = origin + "/";
+ }
+
HttpRequest request = HttpRequest.newBuilder(URI.create(endpoint))
.timeout(Duration.ofSeconds(15))
.header("Accept", "application/json")
.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
+ .header("User-Agent", "open-coding-society-email-service")
+ .header("Origin", origin)
+ .header("Referer", referer)
.POST(HttpRequest.BodyPublishers.ofString(formBody.toString()))
.build();
+ System.out.println("[Email] FormSubmit POST -> " + endpoint + " (Origin: " + origin + ", Referer: " + referer + ")");
+ System.out.println("[Email] Form body: " + formBody.toString());
+
HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
+ System.out.println("[Email] FormSubmit response HTTP " + response.statusCode());
+ String responseBody = response.body();
+ System.out.println("[Email] FormSubmit response body: " + (responseBody == null ? "" : responseBody));
+
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IllegalStateException("FormSubmit returned HTTP " + response.statusCode() + ": " + response.body());
}
+
+ try {
+ JSONObject json = new JSONObject(responseBody == null ? "{}" : responseBody);
+ String success = json.optString("success", "").trim().toLowerCase(Locale.ROOT);
+ if ("false".equals(success)) {
+ String message = json.optString("message", "FormSubmit rejected the request.");
+ throw new IllegalStateException("FormSubmit rejected delivery: " + message);
+ }
+ } catch (org.json.JSONException parseError) {
+ // Accept non-JSON success responses from provider/CDN edge behavior.
+ }
}
private static void sendViaSmtp(String recipient, String subject, String body) {
@@ -206,33 +274,17 @@ public static void sendEmail(String recipient, String subject, Multipart multipa
}
public static void sendEmail(String recipient, String subject, String content){
- String provider = resolveEmailProvider();
+ // Use FormSubmit for deployment safety - no server credentials needed
String safeContent = sanitizeText(content);
try {
- if ("smtp".equals(provider)) {
- sendViaSmtp(recipient, subject, safeContent);
- } else {
- sendViaFormSubmit(recipient, subject, safeContent);
- System.out.println("Mail successfully sent via FormSubmit to " + recipient);
- }
+ System.out.println("[Email] Sending via FormSubmit to " + recipient);
+ sendViaFormSubmit(recipient, subject, safeContent);
+ System.out.println("[Email] SUCCESS via FormSubmit to " + recipient);
} catch (Exception e) {
- System.err.println("Email delivery failed via " + provider + " to " + recipient + ": " + e.getMessage());
+ System.err.println("[Email] FAILED via FormSubmit to " + recipient + ": " + e.getMessage());
e.printStackTrace();
- if (!"smtp".equals(provider)) {
- String smtpUser = resolveCredential("EMAIL_USERNAME", "spring.mail.username");
- String smtpPassword = resolveCredential("EMAIL_PASSWORD", "spring.mail.password");
- if (smtpUser != null && !smtpUser.isBlank() && smtpPassword != null && !smtpPassword.isBlank()) {
- try {
- sendViaSmtp(recipient, subject, safeContent);
- System.out.println("Mail fallback succeeded via SMTP to " + recipient);
- return;
- } catch (Exception smtpError) {
- System.err.println("SMTP fallback also failed for " + recipient + ": " + smtpError.getMessage());
- smtpError.printStackTrace();
- }
- }
- }
+ throw new RuntimeException("Email delivery failed: " + e.getMessage(), e);
}
}
diff --git a/src/main/java/com/open/spring/mvc/slack/EmailNotificationService.java b/src/main/java/com/open/spring/mvc/slack/EmailNotificationService.java
index 5230b40c..40ac5ccd 100644
--- a/src/main/java/com/open/spring/mvc/slack/EmailNotificationService.java
+++ b/src/main/java/com/open/spring/mvc/slack/EmailNotificationService.java
@@ -2,6 +2,7 @@
import java.util.Map;
import java.util.Properties;
+import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
@@ -9,20 +10,26 @@
import org.springframework.stereotype.Service;
import com.open.spring.mvc.comment.Comment;
+import com.open.spring.mvc.comment.CommentJPA;
import com.open.spring.mvc.person.Person;
import com.open.spring.mvc.person.PersonJpaRepository;
import com.open.spring.mvc.person.Email.Email;
import io.github.cdimascio.dotenv.Dotenv;
+import java.util.List;
@Service
public class EmailNotificationService {
private static final Properties APPLICATION_PROPERTIES = loadApplicationProperties();
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
@Autowired
private PersonJpaRepository personRepository;
+ @Autowired
+ private CommentJPA commentJPA;
+
private static Properties loadApplicationProperties() {
Properties props = new Properties();
try (var input = EmailNotificationService.class.getClassLoader().getResourceAsStream("application.properties")) {
@@ -96,6 +103,56 @@ String resolveRecipientEmail() {
return resolveValue("EMAIL_USERNAME", "spring.mail.username");
}
+ private boolean isLikelyEmail(String value) {
+ return value != null && EMAIL_PATTERN.matcher(value.trim()).matches();
+ }
+
+ private String resolveMappedOwnerEmail(String ownerUid) {
+ String mapping = resolveValue("ISSUE_OWNER_EMAIL_MAP", "issue.owner.email-map");
+ if (mapping == null || mapping.isBlank() || ownerUid == null || ownerUid.isBlank()) {
+ return null;
+ }
+
+ for (String pair : mapping.split(",")) {
+ String[] entry = pair.split(":", 2);
+ if (entry.length != 2) {
+ continue;
+ }
+
+ String key = entry[0] == null ? "" : entry[0].trim();
+ String value = entry[1] == null ? "" : entry[1].trim();
+ if (!key.isEmpty() && key.equalsIgnoreCase(ownerUid) && isLikelyEmail(value)) {
+ return value;
+ }
+ }
+
+ return null;
+ }
+
+ private String resolveIssueOwnerEmail(String ownerUid) {
+ if (ownerUid == null || ownerUid.isBlank()) {
+ return null;
+ }
+
+ String ownerKey = ownerUid.trim();
+
+ Person byUid = personRepository.findByUid(ownerKey);
+ if (byUid != null && byUid.getEmail() != null && !byUid.getEmail().isBlank()) {
+ return byUid.getEmail();
+ }
+
+ if (isLikelyEmail(ownerKey)) {
+ return ownerKey;
+ }
+
+ Person byName = personRepository.findByName(ownerKey);
+ if (byName != null && byName.getEmail() != null && !byName.getEmail().isBlank()) {
+ return byName.getEmail();
+ }
+
+ return resolveMappedOwnerEmail(ownerKey);
+ }
+
/**
* Notify configured recipient(s) about a Slack message.
* Priority: logged-in person's email -> FORWARD_PERSON_UID -> FORWARD_EMAIL -> SMTP username -> no-op
@@ -129,18 +186,16 @@ public void notifyOnIssueComment(CalendarIssue issue, Comment comment) {
return;
}
- String issueOwnerUid = issue.getOwnerUid();
- if (issueOwnerUid == null || issueOwnerUid.isBlank()) {
- return;
- }
+ String ownerUid = issue.getOwnerUid();
+ System.out.println("[EmailNotificationService] Issue ID: " + issue.getId() + ", Owner UID from issue: " + ownerUid);
- Person recipient = personRepository.findByUid(issueOwnerUid);
- String recipientEmail = recipient == null ? null : recipient.getEmail();
- if (recipientEmail == null || recipientEmail.isBlank()) {
- recipientEmail = resolveRecipientEmail();
- }
+ // Pull recipient email ONLY from database
+ String recipientEmail = resolveIssueOwnerEmail(ownerUid);
+ System.out.println("[EmailNotificationService] Resolved owner email from database: " + recipientEmail);
+ // Don't fallback to .env - if owner email not in database, skip
if (recipientEmail == null || recipientEmail.isBlank()) {
+ System.out.println("[EmailNotificationService] NO RECIPIENT EMAIL IN DATABASE for owner " + ownerUid + " - skipping send");
return;
}
@@ -154,9 +209,64 @@ public void notifyOnIssueComment(CalendarIssue issue, Comment comment) {
.toString();
try {
+ System.out.println("[EmailNotificationService] SENDING email to " + recipientEmail + " for issue " + issue.getId());
Email.sendEmail(recipientEmail, subject, body);
+ System.out.println("[EmailNotificationService] SUCCESSFULLY SENT to " + recipientEmail + " for issue " + issue.getId());
} catch (Exception ex) {
+ System.err.println("[EmailNotificationService] FAILED to send to " + recipientEmail + " for issue " + issue.getId() + ": " + ex.getMessage());
ex.printStackTrace();
}
}
+
+ /**
+ * Notify all users who starred an issue when a new comment is posted
+ */
+ public void notifyAllStarredIssueFollowers(CalendarIssue issue, Comment comment) {
+ if (issue == null || issue.getId() == null) {
+ return;
+ }
+
+ // Get all "stars" for this issue (use same assignment key format as CommentApiController)
+ String starAssignmentKey = "issue-" + issue.getId() + "::star";
+ List starComments = commentJPA.findByAssignmentAndText(starAssignmentKey, "star");
+
+ if (starComments == null || starComments.isEmpty()) {
+ System.out.println("[EmailNotificationService] No users have starred issue " + issue.getId());
+ return;
+ }
+
+ String issueTitle = issue.getTitle() == null ? "an issue" : issue.getTitle();
+ String subject = "Open Coding Society: new reply on " + issueTitle + " (you starred this)";
+
+ for (Comment starComment : starComments) {
+ String starrerId = starComment.getAuthor();
+ if (starrerId == null || starrerId.equals(comment.getAuthor())) {
+ // Don't send to the person who posted the comment
+ continue;
+ }
+
+ Person starrer = personRepository.findByUid(starrerId);
+ if (starrer == null || starrer.getEmail() == null || starrer.getEmail().isBlank()) {
+ System.out.println("[EmailNotificationService] Starrer " + starrerId + " has no email in database");
+ continue;
+ }
+
+ String recipientEmail = starrer.getEmail();
+ String body = new StringBuilder()
+ .append(comment.getAuthor()).append(" replied to an issue you starred:\\n\\n")
+ .append(issueTitle).append("\\n")
+ .append("Issue ID: ").append(issue.getId()).append("\\n\\n")
+ .append(comment.getText())
+ .toString();
+
+ try {
+ System.out.println("[EmailNotificationService] SENDING to starrer " + recipientEmail + " for issue " + issue.getId());
+ Email.sendEmail(recipientEmail, subject, body);
+ System.out.println("[EmailNotificationService] SUCCESS - starrer notified: " + recipientEmail);
+ } catch (Exception ex) {
+ System.err.println("[EmailNotificationService] FAILED to notify starrer " + recipientEmail + ": " + ex.getMessage());
+ ex.printStackTrace();
+ }
+ }
+ }
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 340959ca..22ccac4a 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -93,6 +93,17 @@ spring.mail.password=vjor zncp naam daph
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
+# Email delivery strategy
+# Default provider for deployed environments; override with EMAIL_PROVIDER=smtp if needed
+email.provider=${EMAIL_PROVIDER:formsubmit}
+# Optional explicit reply-to for outbound messages
+email.replyTo=${EMAIL_REPLY_TO:}
+# FormSubmit endpoint template. Keep {recipient} token to inject issue owner email.
+formsubmit.endpoint=${FORM_SUBMIT_ENDPOINT:https://formsubmit.co/ajax/{recipient}}
+# Optional mapping for owner labels that are not user UIDs.
+# Format: "toby:pranavs22638@gmail.com,alice:alice@example.com"
+issue.owner.email-map=${ISSUE_OWNER_EMAIL_MAP:}
+
# ========== API KEYS ==========
# Google API Key (should be set in .env file for security)
google.api.key=AIzaSyC1xyYXgYfGRJrvMOY8iT2bZnb7-Nyd1HY