mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-20 16:51:03 -05:00
wired in email templates
This commit is contained in:
@@ -46,8 +46,7 @@ public class SecurityConfig {
|
||||
|
||||
// protected
|
||||
.requestMatchers("/api/v1/builds/me/**").authenticated()
|
||||
.requestMatchers("/api/v1/admin/**").authenticated()
|
||||
|
||||
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
|
||||
// everything else (adjust later as you lock down)
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package group.goforward.battlbuilder.controllers;
|
||||
|
||||
import group.goforward.battlbuilder.repos.EmailRequestRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/email")
|
||||
public class EmailTrackingController {
|
||||
|
||||
// 1x1 transparent GIF
|
||||
private static final byte[] PIXEL = new byte[] {
|
||||
71,73,70,56,57,97,1,0,1,0,-128,0,0,0,0,0,-1,-1,-1,33,-7,4,1,0,0,0,0,44,0,0,0,0,1,0,1,0,0,2,2,68,1,0,59
|
||||
};
|
||||
|
||||
private final EmailRequestRepository repo;
|
||||
|
||||
public EmailTrackingController(EmailRequestRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/open/{id}", produces = "image/gif")
|
||||
public ResponseEntity<byte[]> open(@PathVariable Long id) {
|
||||
repo.findById(id).ifPresent(r -> {
|
||||
if (r.getOpenedAt() == null) r.setOpenedAt(LocalDateTime.now());
|
||||
r.setOpenCount((r.getOpenCount() == null ? 0 : r.getOpenCount()) + 1);
|
||||
repo.save(r);
|
||||
});
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
.body(PIXEL);
|
||||
}
|
||||
|
||||
@GetMapping("/click/{id}")
|
||||
public ResponseEntity<Void> click(@PathVariable Long id, @RequestParam String url) {
|
||||
repo.findById(id).ifPresent(r -> {
|
||||
if (r.getClickedAt() == null) r.setClickedAt(LocalDateTime.now());
|
||||
r.setClickCount((r.getClickCount() == null ? 0 : r.getClickCount()) + 1);
|
||||
repo.save(r);
|
||||
});
|
||||
|
||||
return ResponseEntity.status(302).location(URI.create(url)).build();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
package group.goforward.battlbuilder.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "email_requests")
|
||||
@NamedQuery(
|
||||
name = "EmailRequest.findSent",
|
||||
query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.SENT"
|
||||
)
|
||||
|
||||
@NamedQuery(
|
||||
name = "EmailRequest.findFailed",
|
||||
query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.FAILED"
|
||||
)
|
||||
|
||||
@NamedQuery(
|
||||
name = "EmailRequest.findPending",
|
||||
query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.PENDING"
|
||||
)
|
||||
@NamedQueries({
|
||||
@NamedQuery(
|
||||
name = "EmailRequest.findSent",
|
||||
query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.SENT"
|
||||
),
|
||||
@NamedQuery(
|
||||
name = "EmailRequest.findFailed",
|
||||
query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.FAILED"
|
||||
),
|
||||
@NamedQuery(
|
||||
name = "EmailRequest.findPending",
|
||||
query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.PENDING"
|
||||
)
|
||||
})
|
||||
public class EmailRequest {
|
||||
|
||||
@Id
|
||||
@@ -34,101 +35,95 @@ public class EmailRequest {
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String body;
|
||||
|
||||
@Column(name = "sent_at")
|
||||
private LocalDateTime sentAt;
|
||||
@Column(name = "template_key", length = 100)
|
||||
private String templateKey;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private EmailStatus status; // PENDING, SENT, FAILED
|
||||
|
||||
@Column(name = "sent_at")
|
||||
private LocalDateTime sentAt;
|
||||
|
||||
@Column(name = "error_message")
|
||||
private String errorMessage;
|
||||
|
||||
@Column(name = "opened_at")
|
||||
private LocalDateTime openedAt;
|
||||
|
||||
@Column(name = "open_count", nullable = false)
|
||||
private Integer openCount = 0;
|
||||
|
||||
@Column(name = "clicked_at")
|
||||
private LocalDateTime clickedAt;
|
||||
|
||||
@Column(name = "click_count", nullable = false)
|
||||
private Integer clickCount = 0;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
if (status == null) {
|
||||
status = EmailStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
@Column(name = "updated_at", nullable = false, updatable = false)
|
||||
// ✅ should be updatable
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
createdAt = now;
|
||||
updatedAt = now;
|
||||
|
||||
if (status == null) status = EmailStatus.PENDING;
|
||||
if (openCount == null) openCount = 0;
|
||||
if (clickCount == null) clickCount = 0;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public String getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
// ===== Getters / Setters =====
|
||||
|
||||
public void setRecipient(String recipient) {
|
||||
this.recipient = recipient;
|
||||
}
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
public String getRecipient() { return recipient; }
|
||||
public void setRecipient(String recipient) { this.recipient = recipient; }
|
||||
|
||||
public void setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
public String getSubject() { return subject; }
|
||||
public void setSubject(String subject) { this.subject = subject; }
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
public String getBody() { return body; }
|
||||
public void setBody(String body) { this.body = body; }
|
||||
|
||||
public void setBody(String body) {
|
||||
this.body = body;
|
||||
}
|
||||
public String getTemplateKey() { return templateKey; }
|
||||
public void setTemplateKey(String templateKey) { this.templateKey = templateKey; }
|
||||
|
||||
public LocalDateTime getSentAt() {
|
||||
return sentAt;
|
||||
}
|
||||
public EmailStatus getStatus() { return status; }
|
||||
public void setStatus(EmailStatus status) { this.status = status; }
|
||||
|
||||
public void setSentAt(LocalDateTime sentAt) {
|
||||
this.sentAt = sentAt;
|
||||
}
|
||||
public LocalDateTime getSentAt() { return sentAt; }
|
||||
public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; }
|
||||
|
||||
public EmailStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||
|
||||
public void setStatus(EmailStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
public LocalDateTime getOpenedAt() { return openedAt; }
|
||||
public void setOpenedAt(LocalDateTime openedAt) { this.openedAt = openedAt; }
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
public Integer getOpenCount() { return openCount; }
|
||||
public void setOpenCount(Integer openCount) { this.openCount = openCount; }
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
public LocalDateTime getClickedAt() { return clickedAt; }
|
||||
public void setClickedAt(LocalDateTime clickedAt) { this.clickedAt = clickedAt; }
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
public Integer getClickCount() { return clickCount; }
|
||||
public void setClickCount(Integer clickCount) { this.clickCount = clickCount; }
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package group.goforward.battlbuilder.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "email_template")
|
||||
public class EmailTemplate {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "template_key", nullable = false, unique = true, length = 100)
|
||||
private String templateKey;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 150)
|
||||
private String name;
|
||||
|
||||
@Column(name = "subject", nullable = false, length = 255)
|
||||
private String subject;
|
||||
|
||||
@Column(name = "mjml", columnDefinition = "text")
|
||||
private String mjml;
|
||||
|
||||
@Column(name = "html_body", nullable = false, columnDefinition = "text")
|
||||
private String htmlBody;
|
||||
|
||||
@Column(name = "text_body", columnDefinition = "text")
|
||||
private String textBody;
|
||||
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private Boolean enabled = true;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (createdAt == null) createdAt = now;
|
||||
if (updatedAt == null) updatedAt = now;
|
||||
if (enabled == null) enabled = true;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// --- getters/setters ---
|
||||
|
||||
public Long getId() { return id; }
|
||||
|
||||
public String getTemplateKey() { return templateKey; }
|
||||
public void setTemplateKey(String templateKey) { this.templateKey = templateKey; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getSubject() { return subject; }
|
||||
public void setSubject(String subject) { this.subject = subject; }
|
||||
|
||||
public String getMjml() { return mjml; }
|
||||
public void setMjml(String mjml) { this.mjml = mjml; }
|
||||
|
||||
public String getHtmlBody() { return htmlBody; }
|
||||
public void setHtmlBody(String htmlBody) { this.htmlBody = htmlBody; }
|
||||
|
||||
public String getTextBody() { return textBody; }
|
||||
public void setTextBody(String textBody) { this.textBody = textBody; }
|
||||
|
||||
public Boolean getEnabled() { return enabled; }
|
||||
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package group.goforward.battlbuilder.repos;
|
||||
|
||||
import group.goforward.battlbuilder.model.EmailTemplate;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface EmailTemplateRepository extends JpaRepository<EmailTemplate, Long> {
|
||||
|
||||
Optional<EmailTemplate> findByTemplateKeyAndEnabledTrue(String templateKey);
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import group.goforward.battlbuilder.model.AuthToken;
|
||||
import group.goforward.battlbuilder.model.User;
|
||||
import group.goforward.battlbuilder.repos.AuthTokenRepository;
|
||||
import group.goforward.battlbuilder.repos.UserRepository;
|
||||
import group.goforward.battlbuilder.services.utils.EmailService;
|
||||
import group.goforward.battlbuilder.services.utils.TemplatedEmailService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -14,13 +14,14 @@ import java.security.SecureRandom;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class BetaInviteService {
|
||||
|
||||
private final UserRepository users;
|
||||
private final AuthTokenRepository tokens;
|
||||
private final EmailService emailService;
|
||||
private final TemplatedEmailService templatedEmailService;
|
||||
|
||||
@Value("${app.publicBaseUrl:http://localhost:3000}")
|
||||
private String publicBaseUrl;
|
||||
@@ -30,15 +31,19 @@ public class BetaInviteService {
|
||||
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
public BetaInviteService(UserRepository users, AuthTokenRepository tokens, EmailService emailService) {
|
||||
// ✅ Constructor injection
|
||||
public BetaInviteService(
|
||||
UserRepository users,
|
||||
AuthTokenRepository tokens,
|
||||
TemplatedEmailService templatedEmailService
|
||||
) {
|
||||
this.users = users;
|
||||
this.tokens = tokens;
|
||||
this.emailService = emailService;
|
||||
this.templatedEmailService = templatedEmailService;
|
||||
}
|
||||
|
||||
public int inviteAllBetaUsers(int tokenMinutes, int limit, boolean dryRun) {
|
||||
// You may need to adjust this query depending on your repo methods.
|
||||
// See NOTE below if you don’t have this finder.
|
||||
|
||||
List<User> betaUsers = (limit > 0)
|
||||
? users.findTopNByRoleAndIsActiveFalseAndDeletedAtIsNull("BETA", limit)
|
||||
: users.findByRoleAndIsActiveFalseAndDeletedAtIsNull("BETA");
|
||||
@@ -49,23 +54,24 @@ public class BetaInviteService {
|
||||
String email = user.getEmail();
|
||||
|
||||
String magicToken = generateToken();
|
||||
saveToken(email, AuthToken.TokenType.MAGIC_LOGIN, magicToken,
|
||||
OffsetDateTime.now().plusMinutes(tokenMinutes));
|
||||
saveToken(
|
||||
email,
|
||||
AuthToken.TokenType.MAGIC_LOGIN,
|
||||
magicToken,
|
||||
OffsetDateTime.now().plusMinutes(tokenMinutes)
|
||||
);
|
||||
|
||||
String magicUrl = publicBaseUrl + "/beta/magic?token=" + magicToken;
|
||||
|
||||
String subject = "Your Battl Builders beta access link";
|
||||
String body = """
|
||||
You’re in.
|
||||
|
||||
Here’s your secure sign-in link (expires in %d minutes):
|
||||
%s
|
||||
|
||||
If you didn’t request this, you can ignore this email.
|
||||
""".formatted(tokenMinutes, magicUrl);
|
||||
|
||||
if (!dryRun) {
|
||||
emailService.sendEmail(email, subject, body);
|
||||
templatedEmailService.send(
|
||||
"beta_invite", // template_key
|
||||
email,
|
||||
Map.of(
|
||||
"minutes", String.valueOf(tokenMinutes),
|
||||
"magicUrl", magicUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
sent++;
|
||||
@@ -74,7 +80,12 @@ public class BetaInviteService {
|
||||
return sent;
|
||||
}
|
||||
|
||||
private void saveToken(String email, AuthToken.TokenType type, String token, OffsetDateTime expiresAt) {
|
||||
private void saveToken(
|
||||
String email,
|
||||
AuthToken.TokenType type,
|
||||
String token,
|
||||
OffsetDateTime expiresAt
|
||||
) {
|
||||
AuthToken t = new AuthToken();
|
||||
t.setEmail(email);
|
||||
t.setType(type);
|
||||
@@ -93,7 +104,9 @@ public class BetaInviteService {
|
||||
private String hashToken(String token) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hashed = md.digest((tokenPepper + ":" + token).getBytes(StandardCharsets.UTF_8));
|
||||
byte[] hashed = md.digest(
|
||||
(tokenPepper + ":" + token).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
return HexFormat.of().formatHex(hashed);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to hash token", e);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package group.goforward.battlbuilder.services.utils;
|
||||
|
||||
import aj.org.objectweb.asm.commons.Remapper;
|
||||
import group.goforward.battlbuilder.model.EmailRequest;
|
||||
|
||||
public interface EmailService {
|
||||
EmailRequest sendEmail(String recipient, String subject, String body);
|
||||
|
||||
EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody);
|
||||
|
||||
EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody, String templateKey);
|
||||
|
||||
void deleteById(Integer id);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package group.goforward.battlbuilder.services.utils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class TemplateRenderer {
|
||||
private TemplateRenderer() {}
|
||||
|
||||
public static String render(String template, Map<String, String> vars) {
|
||||
String out = template;
|
||||
for (var e : vars.entrySet()) {
|
||||
out = out.replace("{{" + e.getKey() + "}}", e.getValue() == null ? "" : e.getValue());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package group.goforward.battlbuilder.services.utils;
|
||||
|
||||
import group.goforward.battlbuilder.model.EmailRequest;
|
||||
import group.goforward.battlbuilder.model.EmailTemplate;
|
||||
import group.goforward.battlbuilder.repos.EmailTemplateRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class TemplatedEmailService {
|
||||
|
||||
private final EmailTemplateRepository templates;
|
||||
private final EmailService emailService;
|
||||
|
||||
public TemplatedEmailService(EmailTemplateRepository templates, EmailService emailService) {
|
||||
this.templates = templates;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
public EmailRequest send(String templateKey, String to, Map<String, String> vars) {
|
||||
EmailTemplate t = templates.findByTemplateKeyAndEnabledTrue(templateKey)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing/disabled email template: " + templateKey));
|
||||
|
||||
String subject = TemplateRenderer.render(t.getSubject(), vars);
|
||||
String html = TemplateRenderer.render(t.getHtmlBody(), vars);
|
||||
String text = t.getTextBody() == null ? null : TemplateRenderer.render(t.getTextBody(), vars);
|
||||
|
||||
// ✅ template_key persisted inside EmailService (no double-save)
|
||||
return emailService.sendEmailHtml(to, subject, html, text, templateKey);
|
||||
}
|
||||
}
|
||||
@@ -31,38 +31,81 @@ public class EmailServiceImpl implements EmailService {
|
||||
@Value("${app.email.outbound-enabled:true}")
|
||||
private boolean outboundEnabled;
|
||||
|
||||
/**
|
||||
* Sends an email and persists send status.
|
||||
* Uses multipart=true to avoid MimeMessageHelper errors when setting text.
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public EmailRequest sendEmail(String recipient, String subject, String body) {
|
||||
EmailRequest req = new EmailRequest();
|
||||
req.setRecipient(recipient);
|
||||
req.setSubject(subject);
|
||||
req.setBody(body);
|
||||
req.setStatus(EmailStatus.PENDING);
|
||||
|
||||
// Persist initial request
|
||||
EmailRequest emailRequest = new EmailRequest();
|
||||
emailRequest.setRecipient(recipient);
|
||||
emailRequest.setSubject(subject);
|
||||
emailRequest.setBody(body);
|
||||
emailRequest.setStatus(EmailStatus.PENDING);
|
||||
emailRequest.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
emailRequest = emailRequestRepository.save(emailRequest);
|
||||
|
||||
// ✅ Capture-only mode: record it, but don’t send
|
||||
// Capture-only mode: store but don’t send
|
||||
if (!outboundEnabled) {
|
||||
emailRequest.setStatus(EmailStatus.FAILED);
|
||||
emailRequest.setErrorMessage("Outbound email suppressed by config (app.email.outbound-enabled=false)");
|
||||
return emailRequestRepository.save(emailRequest);
|
||||
req.setStatus(EmailStatus.PENDING);
|
||||
req.setErrorMessage("Outbound email suppressed by config (app.email.outbound-enabled=false)");
|
||||
return emailRequestRepository.save(req);
|
||||
}
|
||||
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
|
||||
// ✅ multipart=true fixes "Not in multipart mode" errors
|
||||
MimeMessageHelper helper = new MimeMessageHelper(
|
||||
message,
|
||||
true, // multipart
|
||||
true,
|
||||
StandardCharsets.UTF_8.name()
|
||||
);
|
||||
|
||||
helper.setFrom(fromEmail);
|
||||
helper.setTo(recipient);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(body, false);
|
||||
|
||||
mailSender.send(message);
|
||||
|
||||
req.setStatus(EmailStatus.SENT);
|
||||
req.setSentAt(LocalDateTime.now());
|
||||
} catch (Exception e) {
|
||||
req.setStatus(EmailStatus.FAILED);
|
||||
req.setErrorMessage(e.getMessage());
|
||||
}
|
||||
|
||||
// ✅ Single INSERT (no update spam)
|
||||
return emailRequestRepository.save(req);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(Integer id) {
|
||||
emailRequestRepository.deleteById(id.longValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody) {
|
||||
return sendEmailHtml(recipient, subject, htmlBody, textBody, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody, String templateKey) {
|
||||
|
||||
EmailRequest req = new EmailRequest();
|
||||
req.setRecipient(recipient);
|
||||
req.setSubject(subject);
|
||||
req.setBody(htmlBody); // storing HTML for now
|
||||
req.setTemplateKey(templateKey);
|
||||
req.setStatus(EmailStatus.PENDING);
|
||||
|
||||
// Capture-only mode: store but don’t send
|
||||
if (!outboundEnabled) {
|
||||
req.setStatus(EmailStatus.PENDING);
|
||||
req.setErrorMessage("Outbound email suppressed by config (app.email.outbound-enabled=false)");
|
||||
return emailRequestRepository.save(req);
|
||||
}
|
||||
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(
|
||||
message,
|
||||
true,
|
||||
StandardCharsets.UTF_8.name()
|
||||
);
|
||||
|
||||
@@ -70,24 +113,19 @@ public class EmailServiceImpl implements EmailService {
|
||||
helper.setTo(recipient);
|
||||
helper.setSubject(subject);
|
||||
|
||||
// Plain text email (safe + deliverable)
|
||||
helper.setText(body, false);
|
||||
// plain + html (best practice)
|
||||
helper.setText(textBody != null ? textBody : "", htmlBody);
|
||||
|
||||
mailSender.send(message);
|
||||
|
||||
emailRequest.setStatus(EmailStatus.SENT);
|
||||
emailRequest.setSentAt(LocalDateTime.now());
|
||||
|
||||
req.setStatus(EmailStatus.SENT);
|
||||
req.setSentAt(LocalDateTime.now());
|
||||
} catch (Exception e) {
|
||||
emailRequest.setStatus(EmailStatus.FAILED);
|
||||
emailRequest.setErrorMessage(e.getMessage());
|
||||
req.setStatus(EmailStatus.FAILED);
|
||||
req.setErrorMessage(e.getMessage());
|
||||
}
|
||||
|
||||
return emailRequestRepository.save(emailRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(Integer id) {
|
||||
emailRequestRepository.deleteById(id.longValue());
|
||||
// ✅ Single INSERT (no extra UPDATEs)
|
||||
return emailRequestRepository.save(req);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ security.jwt.access-token-days=30
|
||||
|
||||
# Beta Invite Email Toggle
|
||||
app.beta.captureOnly=true
|
||||
app.email.outbound-enabled=false
|
||||
app.email.outbound-enabled=true
|
||||
|
||||
# CLI invite runner (off by default)
|
||||
app.beta.invite.run=false
|
||||
|
||||
Reference in New Issue
Block a user