diff --git a/pom.xml b/pom.xml index 89f296a2..de7a6dfe 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,8 @@ + + net.java.dev.jna @@ -206,11 +208,38 @@ json 20180813 + + + + + + org.mockito mockito-core - 3.2.0 - compile + 4.11.0 + test + + + + org.mockito + mockito-inline + 4.11.0 + test + + + + net.bytebuddy + byte-buddy + 1.14.11 + test + + + + net.bytebuddy + byte-buddy-agent + 1.14.11 + test org.junit.jupiter @@ -236,11 +265,7 @@ 5.7.0 test - - net.bytebuddy - byte-buddy - 1.10.4 - + org.slf4j slf4j-api @@ -317,5 +342,16 @@ braintree-java 3.11.0 + + com.sendgrid + sendgrid-java + 4.9.3 + + + + org.springframework + spring-context + 5.3.33 + \ No newline at end of file diff --git a/src/main/Activity/Activity.java b/src/main/Activity/Activity.java index ead1a583..7a6ab0b1 100644 --- a/src/main/Activity/Activity.java +++ b/src/main/Activity/Activity.java @@ -13,6 +13,12 @@ public class Activity implements Comparable { private ObjectId id; + private LocalDateTime notifiedAt; + + + // add notification + @BsonProperty(value = "notified") + private boolean notified = false; @BsonProperty(value = "occurredAt") private LocalDateTime occurredAt; @@ -64,6 +70,13 @@ public String getUsername() { public ObjectId getId() { return id; } + public LocalDateTime getNotifiedAt() { + return notifiedAt; + } + + public void setNotifiedAt(LocalDateTime notifiedAt) { + this.notifiedAt = notifiedAt; + } public Activity setUsername(String username) { this.username = username; @@ -74,6 +87,14 @@ public void setId(ObjectId id) { this.id = id; } + public boolean isNotified() { + return notified; + } + + public void setNotified(boolean notified) { + this.notified = notified; + } + // default sort is by occurred at, and then by username private Comparator getComparator() { return Comparator.comparing(Activity::getOccurredAt) diff --git a/src/main/App.java b/src/main/App.java index eca3dba1..1944c903 100644 --- a/src/main/App.java +++ b/src/main/App.java @@ -6,3 +6,6 @@ public static void main(String[] args) { AppConfig.appFactory(DeploymentLevel.STAGING); } } + + + diff --git a/src/main/Config/AppConfig.java b/src/main/Config/AppConfig.java index f72ea142..e8b6665b 100644 --- a/src/main/Config/AppConfig.java +++ b/src/main/Config/AppConfig.java @@ -5,6 +5,7 @@ import Billing.BillingController; import Database.Activity.ActivityDao; import Database.Activity.ActivityDaoFactory; +import Database.Activity.ActivityDaoImpl; import Database.File.FileDao; import Database.File.FileDaoFactory; import Database.Form.FormDao; @@ -24,6 +25,7 @@ import Issue.IssueController; import Mail.FileBackfillController; import Mail.MailController; +import Mail.ScheduledEmailDispatcher; import OptionalUserInformation.OptionalUserInformationController; import Organization.Organization; import Organization.OrganizationController; @@ -40,10 +42,21 @@ import com.mongodb.client.MongoDatabase; import io.javalin.Javalin; import io.javalin.http.HttpResponseException; + +import java.time.Duration; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + import lombok.SneakyThrows; import org.bson.types.ObjectId; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + public class AppConfig { public static Long ASYNC_TIME_OUT = 10L; @@ -52,9 +65,20 @@ public class AppConfig { @SneakyThrows public static Javalin appFactory(DeploymentLevel deploymentLevel) { - System.setProperty("logback.configurationFile", "../Logger/Resources/logback.xml"); + System.setProperty("logback.configurationFile", "../Logger/Resources/logback.xml"); + MongoConfig.getMongoClient(); + ActivityDao activityDao = ActivityDaoFactory.create(deploymentLevel); + ScheduledEmailDispatcher dispatcher = new ScheduledEmailDispatcher(activityDao); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleAtFixedRate( + dispatcher::dispatchDailyReminders, + computeInitialDelayTo8AM(), + TimeUnit.DAYS.toSeconds(1), + TimeUnit.SECONDS + ); + // create app Javalin app = AppConfig.createJavalinApp(deploymentLevel); - MongoConfig.getMongoClient(); + // Continue loading other DAOs UserDao userDao = UserDaoFactory.create(deploymentLevel); OptionalUserInformationDao optionalUserInformationDao = OptionalUserInformationDaoFactory.create(deploymentLevel); @@ -62,7 +86,7 @@ public static Javalin appFactory(DeploymentLevel deploymentLevel) { OrgDao orgDao = OrgDaoFactory.create(deploymentLevel); FormDao formDao = FormDaoFactory.create(deploymentLevel); FileDao fileDao = FileDaoFactory.create(deploymentLevel); - ActivityDao activityDao = ActivityDaoFactory.create(deploymentLevel); + //ActivityDao activityDao = ActivityDaoFactory.create(deploymentLevel); MailDao mailDao = MailDaoFactory.create(deploymentLevel); MongoDatabase db = MongoConfig.getDatabase(deploymentLevel); setApplicationHeaders(app); @@ -283,6 +307,18 @@ public static Javalin appFactory(DeploymentLevel deploymentLevel) { app.post("/submit-mail", mailController.saveMail); return app; } + private static long computeInitialDelayTo8AM() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime nextRun = now.withHour(8).withMinute(0).withSecond(0).withNano(0); + + if (now.isAfter(nextRun)) { + // if now the time is over 8:00 am, schedule to tomorrow + nextRun = nextRun.plusDays(1); + } + + Duration delay = Duration.between(now, nextRun); + return delay.getSeconds(); // return as second + } public static void setApplicationHeaders(Javalin app) { app.before( diff --git a/src/main/Database/Activity/ActivityDao.java b/src/main/Database/Activity/ActivityDao.java index 1ad82bcd..c5ae94f1 100644 --- a/src/main/Database/Activity/ActivityDao.java +++ b/src/main/Database/Activity/ActivityDao.java @@ -12,4 +12,11 @@ public interface ActivityDao extends Dao { List getAllFromUserBetweenInclusive( String username, LocalDateTime startTime, LocalDateTime endTime); + // New: Find up to N unnotified activities (for email reminders) + List findUnnotified(int limit); + + // New: Update an activity (to mark as notified) + void update(Activity activity); + List getUnnotifiedActivities(); } + diff --git a/src/main/Database/Activity/ActivityDaoImpl.java b/src/main/Database/Activity/ActivityDaoImpl.java index 649adf3e..00f468c0 100644 --- a/src/main/Database/Activity/ActivityDaoImpl.java +++ b/src/main/Database/Activity/ActivityDaoImpl.java @@ -3,9 +3,12 @@ import Activity.Activity; import Config.DeploymentLevel; import Config.MongoConfig; +import Mail.EmailNotifier; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; import org.bson.types.ObjectId; +import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.ArrayList; @@ -16,6 +19,7 @@ import static com.mongodb.client.model.Filters.eq; +@Repository public class ActivityDaoImpl implements ActivityDao { private final MongoCollection activityCollection; @@ -57,6 +61,15 @@ public List getAll() { .sorted(Comparator.reverseOrder()) .collect(Collectors.toList()); } + // + // @Override + public List getUnnotifiedActivities() { + return activityCollection.find(eq("notified", false)) + .into(new ArrayList<>()).stream() + .sorted(Comparator.reverseOrder()) // optional + .collect(Collectors.toList()); + } + @Override public int size() { @@ -80,6 +93,18 @@ public void update(Activity activity) { @Override public void save(Activity activity) { + activityCollection.insertOne(activity); + // Trigger email notifications + EmailNotifier.handle(activity);; } + @Override + public List findUnnotified(int limit) { + return activityCollection.find(eq("notified", false)) + .limit(limit) + .into(new ArrayList<>()).stream() + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + } + } diff --git a/src/main/Database/Activity/ActivityDaoTestImpl.java b/src/main/Database/Activity/ActivityDaoTestImpl.java index c2fe31b1..60951fa6 100644 --- a/src/main/Database/Activity/ActivityDaoTestImpl.java +++ b/src/main/Database/Activity/ActivityDaoTestImpl.java @@ -75,4 +75,14 @@ public void update(Activity activity) { public void save(Activity activity) { activityMap.put(activity.getId(), activity); } + + @Override + public List findUnnotified(int limit) { + return new ArrayList<>(); + } + @Override + public List getUnnotifiedActivities() { + return Collections.emptyList(); + } + } diff --git a/src/main/Mail/EmailNotifier.java b/src/main/Mail/EmailNotifier.java new file mode 100644 index 00000000..4fbceb3f --- /dev/null +++ b/src/main/Mail/EmailNotifier.java @@ -0,0 +1,45 @@ +package Mail; + +import Activity.Activity; +import Mail.Services.SendgridService; + +import java.util.List; + +public class EmailNotifier { + + public static void handle(Activity activity) { + List types = activity.getType(); + if (types == null || types.isEmpty()) return; + + String type = types.get(types.size() - 1); // get the most specific activity type + // System.out.println("Handling activity type: " + type + " for user: " + activity.getUsername()); + switch (type) { + case "CreateClientActivity": + SendgridService.handleCreateClientActivity(activity.getUsername(), "PA"); // placeholder + break; + case "UploadFileActivity": + SendgridService.handleUploadFileActivity(activity.getUsername(), "DocumentType"); // placeholder + break; + case "StartApplicationActivity": + SendgridService.handleMailApplicationActivity(activity.getUsername()); + break; + case "MailApplicationActivity": + SendgridService.handleSubmitApplicationActivity(activity.getUsername(), "NonprofitName"); // placeholder + break; +// case "SubmitApplicationActivity": +// SendgridService.sendSubmissionConfirmation(user, "ApplicationName"); +// break; +// case "RecoverPasswordActivity": +// SendgridService.sendPasswordRecoveryAlert(user); +// break; +// case "ChangePasswordActivity": +// SendgridService.sendPasswordChangeConfirmation(user); +// break; +// case "Change2FAActivity": +// SendgridService.send2FAUpdateNotice(user, activity.getObjectName()); // "on"/"off" +// break; + default: + // Optional: log unknown type + } + } +} diff --git a/src/main/Mail/ScheduledEmailDispatcher.java b/src/main/Mail/ScheduledEmailDispatcher.java new file mode 100644 index 00000000..b861b640 --- /dev/null +++ b/src/main/Mail/ScheduledEmailDispatcher.java @@ -0,0 +1,30 @@ +package Mail; + +import Activity.Activity; +import Database.Activity.ActivityDao; + +import java.time.LocalDateTime; +import java.util.List; + +public class ScheduledEmailDispatcher { + private final ActivityDao activityDao; + + public ScheduledEmailDispatcher(ActivityDao activityDao) { + this.activityDao = activityDao; + } + + public void dispatchDailyReminders() { + System.out.println("Running scheduled reminder task: " + LocalDateTime.now()); + + List unnotified = activityDao.getUnnotifiedActivities(); + for (Activity activity : unnotified) { + // + EmailNotifier.handle(activity); + + activity.setNotified(true); + activity.setNotifiedAt(LocalDateTime.now()); + activityDao.update(activity); + } + } +} + diff --git a/src/main/Mail/Services/SendgridService.java b/src/main/Mail/Services/SendgridService.java new file mode 100644 index 00000000..d8ea9e78 --- /dev/null +++ b/src/main/Mail/Services/SendgridService.java @@ -0,0 +1,112 @@ +package Mail.Services; + +import com.sendgrid.*; +import com.sendgrid.helpers.mail.objects.Content; +import com.sendgrid.helpers.mail.objects.Email; +import com.sendgrid.helpers.mail.Mail; +import io.github.cdimascio.dotenv.Dotenv; + +import java.util.Map; +import java.util.Optional; +import Mail.Utils.TemplateEngine; + + +import java.io.IOException; + +public class SendgridService { + private static final String SENDGRID_API_KEY = + Optional.ofNullable(System.getenv("SENDGRID_API_KEY")) + .orElseGet(() -> Dotenv.configure() + .ignoreIfMissing() + .load() + .get("SENDGRID_API_KEY")); + + //private static final String SENDGRID_API_KEY = System.getenv("SENDGRID_API_KEY"); + + private static void sendEmail(String toEmail, String subject, String bodyHtml) { + Email from = new Email("vanessachung@keep.id"); + Email to = new Email(toEmail); + Content content = new Content("text/html", bodyHtml); + Mail mail = new Mail(from, subject, to, content); + + SendGrid sg = new SendGrid(SENDGRID_API_KEY); + Request request = new Request(); + + try { + request.setMethod(Method.POST); + request.setEndpoint("mail/send"); + request.setBody(mail.build()); + Response response = sg.api(request); + + if (response.getStatusCode() >= 400) { + System.err.println("Email failed: " + response.getBody()); + } + } catch (IOException ex) { + System.err.println("SendGrid Exception: " + ex.getMessage()); + } + } + // Triggered by CreateClientActivity + public static void handleCreateClientActivity(String username, String state) { + try { + String body = TemplateEngine.renderTemplate("welcome.html", Map.of( + "username", username, + "state", state + )); + sendEmail(username, "Welcome to Keep.id!", body); + } catch (IOException e) { + System.err.println("Failed to load welcome template: " + e.getMessage()); + } + } + // Triggered by UploadFileActivity + public static void handleUploadFileActivity(String username, String docType) { + String subject = "You uploaded a " + docType; + String body = "

We’ve saved your " + docType + ". Here's what to upload next…

"; + sendEmail(username, subject, body); + } + + // Triggered by MailApplicationActivity + public static void handleMailApplicationActivity(String username) { + String subject = "Finish your application"; + String body = "

You started an application. Come back and finish it!

"; + sendEmail(username, subject, body); + } + + // Triggered by SubmitApplicationActivity + public static void handleSubmitApplicationActivity(String username, String nonprofit) { + String subject = "Pick up your ID"; + String body = "

Your documents are ready! Go to " + nonprofit + " to pick them up.

"; + sendEmail(username, subject, body); + } + + // For debugging + public static void sendTestEmail() { + String subject = "Test from Keep.id"; + String body = "

Hello! This is a test email sent via SendGrid!

"; + sendEmail("vanessachung@keep.id", subject, body); + } +// public static void sendWelcomeWithQuickStart(String username, String state) { +// String subject = "Welcome to Keep.id!"; +// String body = "

Hi " + username + ", welcome! Here's how to get your ID in " + state + "...

"; +// sendEmail(username, subject, body); // Replace with real email +// } +// +// public static void sendUploadReminder(String username, String docType) { +// String subject = "You uploaded a " + docType; +// String body = "

We’ve saved your " + docType + ". Here's what to upload next…

"; +// sendEmail(username, subject, body); +// } +// +// public static void sendApplicationReminder(String username) { +// sendEmail(username , "Finish your application", "

You started an application. Come back and finish it!

"); +// } +// +// public static void sendPickupInfo(String username, String nonprofit) { +// sendEmail(username , "Pick up your ID", "

Your documents are ready! Go to " + nonprofit + " to pick them up.

"); +// } +// public static void sendTestEmail() { +// String subject = "Test from Keep.id"; +// String body = "

Hello! This is a test email sent via SendGrid!

"; +// sendEmail("vanessachung@keep.id", subject, body); +// } + +} diff --git a/src/main/Mail/Utils/TemplateEngine.java b/src/main/Mail/Utils/TemplateEngine.java new file mode 100644 index 00000000..95e6c717 --- /dev/null +++ b/src/main/Mail/Utils/TemplateEngine.java @@ -0,0 +1,20 @@ +package Mail.Utils; + + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; + +public class TemplateEngine { + public static String renderTemplate(String templateName, Map variables) throws IOException { + String templatePath = "src/main/Mail/email_templates/" + templateName; + String content = Files.readString(Paths.get(templatePath)); + + for (Map.Entry entry : variables.entrySet()) { + content = content.replace("{{" + entry.getKey() + "}}", entry.getValue()); + } + + return content; + } +} diff --git a/src/main/Mail/email_templates/upload_reminder.html b/src/main/Mail/email_templates/upload_reminder.html new file mode 100644 index 00000000..ebfb3243 --- /dev/null +++ b/src/main/Mail/email_templates/upload_reminder.html @@ -0,0 +1 @@ +

We’ve saved your {{docType}}. Here's what to upload next…

\ No newline at end of file diff --git a/src/main/Mail/email_templates/welcome.html b/src/main/Mail/email_templates/welcome.html new file mode 100644 index 00000000..baae1b48 --- /dev/null +++ b/src/main/Mail/email_templates/welcome.html @@ -0,0 +1,62 @@ + + + + + Welcome to Keep.id! + + + + + + + + + +
+

+ + Welcome, {{username}}! +

+

We're excited to have you join Keep.id — your secure and simple way to manage your identity + online.

+ +

To get started with your ID application in {{state}}, here’s what you can do next:

+ + +
    +
  • + + Complete your profile and required documents +
  • +
  • + + Upload scans or photos of your identification +
  • +
  • + + Pick a nonprofit location for pickup +
  • +
+ +

If you have any questions, feel free to reach out to our support team.

+ + +
+ 2025-07-14-7-57-24 +

We’re here to help you every step of the way.

+
+ +

Welcome aboard!
The Keep.id Team

+
+ + +
+ © {2020} Keep.id – All rights reserved. +
+ + diff --git a/src/test/DatabaseTest/Activity/ActivityDaoImplUnitTests.java b/src/test/DatabaseTest/Activity/ActivityDaoImplUnitTests.java index e6347896..f07b88ea 100644 --- a/src/test/DatabaseTest/Activity/ActivityDaoImplUnitTests.java +++ b/src/test/DatabaseTest/Activity/ActivityDaoImplUnitTests.java @@ -7,12 +7,17 @@ import Database.Activity.ActivityDao; import Database.Activity.ActivityDaoFactory; import File.FileType; +import Mail.EmailNotifier; import TestUtils.EntityFactory; import TestUtils.TestUtils; import java.time.LocalDateTime; +import java.util.Arrays; import java.util.List; import org.bson.types.ObjectId; import org.junit.*; +import static org.mockito.Mockito.*; +import org.mockito.MockedStatic; + public class ActivityDaoImplUnitTests { private ActivityDao activityDao; @@ -247,4 +252,21 @@ static boolean areActivitiesEqual(List activities1, List act } return isEqual; } + +// @Test +// public void testSaveCallsEmailNotifierHandle() { +// Activity activity = new Activity(); +// activity.setUsername("testuser"); +// activity.setType(Arrays.asList("Activity", "UploadFileActivity")); +// +// try (MockedStatic mockedNotifier = mockStatic(EmailNotifier.class)) { +// +// activityDao.save(activity); +// +// +// mockedNotifier.verify(() -> EmailNotifier.handle(activity), times(1)); +// } +// Activity found = activityDao.get(activity.getId()).get(); +// assertEquals("testuser", found.getUsername()); +// } } diff --git a/src/test/EmailTest/EmailIntTest.java b/src/test/EmailTest/EmailIntTest.java new file mode 100644 index 00000000..e031721c --- /dev/null +++ b/src/test/EmailTest/EmailIntTest.java @@ -0,0 +1,37 @@ +package EmailTest; + +import Activity.Activity; +import Mail.EmailNotifier; +import java.util.Arrays; +import org.junit.Test; + +public class EmailIntTest { + + @Test + public void testSendEmailsForAllActivities() { + + // change the email to test + String testEmail = "vanessachung@keep.id"; + + Activity createClient = new Activity(); + createClient.setUsername(testEmail); + createClient.setType(Arrays.asList("Activity", "CreateClientActivity")); + EmailNotifier.handle(createClient); + +// Activity uploadFile = new Activity(); +// uploadFile.setUsername(testEmail); +// uploadFile.setType(Arrays.asList("Activity", "UploadFileActivity")); +// EmailNotifier.handle(uploadFile); +// +// Activity submitApp = new Activity(); +// submitApp.setUsername(testEmail); +// submitApp.setType(Arrays.asList("Activity", "SubmitApplicationActivity")); +// EmailNotifier.handle(submitApp); +// +// Activity mailApp = new Activity(); +// mailApp.setUsername(testEmail); +// mailApp.setType(Arrays.asList("Activity", "MailApplicationActivity")); +// EmailNotifier.handle(mailApp); + } +} + diff --git a/src/test/EmailTest/EmailNotifierTest.java b/src/test/EmailTest/EmailNotifierTest.java new file mode 100644 index 00000000..3a4b7f75 --- /dev/null +++ b/src/test/EmailTest/EmailNotifierTest.java @@ -0,0 +1,68 @@ +package EmailTest; + +import Mail.EmailNotifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import Activity.Activity; +import Mail.Services.SendgridService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import java.util.Arrays; +import static org.mockito.Mockito.*; + +public class EmailNotifierTest { + + Activity activity; + + @BeforeEach + public void setUp() { + activity = new Activity(); + activity.setUsername("testuser"); + } + + @Test + public void testCreateClientActivity() { + activity.setType(Arrays.asList("Activity", "CreateClientActivity")); + try (MockedStatic mocked = mockStatic(SendgridService.class)) { + EmailNotifier.handle(activity); + mocked.verify(() -> SendgridService.handleCreateClientActivity("testuser", "PA")); + } + } + + @Test + public void testUploadFileActivity() { + activity.setType(Arrays.asList("Activity", "UploadFileActivity")); + try (MockedStatic mocked = mockStatic(SendgridService.class)) { + EmailNotifier.handle(activity); + mocked.verify(() -> SendgridService.handleUploadFileActivity("testuser", "DocumentType")); + } + } + + @Test + public void testMailApplicationActivity() { + activity.setType(Arrays.asList("Activity", "MailApplicationActivity")); + try (MockedStatic mocked = mockStatic(SendgridService.class)) { + EmailNotifier.handle(activity); + mocked.verify(() -> SendgridService.handleMailApplicationActivity("testuser")); + } + } + + @Test + public void testSubmitApplicationActivity() { + activity.setType(Arrays.asList("Activity", "SubmitApplicationActivity")); + try (MockedStatic mocked = mockStatic(SendgridService.class)) { + EmailNotifier.handle(activity); + mocked.verify(() -> SendgridService.handleSubmitApplicationActivity("testuser", "NonprofitName")); + } + } + + @Test + public void testUnknownActivityType() { + activity.setType(Arrays.asList("Activity", "SomethingElse")); + try (MockedStatic mocked = mockStatic(SendgridService.class)) { + EmailNotifier.handle(activity); + mocked.verifyNoInteractions(); + } + } +} diff --git a/src/test/EmailTest/SendgridManualTest.java b/src/test/EmailTest/SendgridManualTest.java new file mode 100644 index 00000000..1229ac8e --- /dev/null +++ b/src/test/EmailTest/SendgridManualTest.java @@ -0,0 +1,44 @@ +package EmailTest; + + +import com.sendgrid.*; +import com.sendgrid.helpers.mail.Mail; +import com.sendgrid.helpers.mail.objects.Content; +import com.sendgrid.helpers.mail.objects.Email; +import io.github.cdimascio.dotenv.Dotenv; + +import java.io.IOException; +// ensure sendgrid API is connected +public class SendgridManualTest { + public static void main(String[] args) { + // manually load .env + Dotenv dotenv = Dotenv.load(); + String SENDGRID_API_KEY = dotenv.get("SENDGRID_API_KEY"); + + if (SENDGRID_API_KEY == null) { + System.err.println("SENDGRID_API_KEY is missing or .env not loaded."); + return; + } + + Email from = new Email("vanessachung@keep.id"); // sender + Email to = new Email("vanessa137222@gmail.com"); + String subject = "Manual Test Email from KeepID"; + Content content = new Content("text/plain", "This is a test email sent directly via SendGrid."); + Mail mail = new Mail(from, subject, to, content); + + SendGrid sg = new SendGrid(SENDGRID_API_KEY); + Request request = new Request(); + + try { + request.setMethod(Method.POST); + request.setEndpoint("mail/send"); + request.setBody(mail.build()); + Response response = sg.api(request); + + System.out.println("Status code: " + response.getStatusCode()); + System.out.println("Response body: " + response.getBody()); + } catch (IOException ex) { + System.err.println("Failed to send email: " + ex.getMessage()); + } + } +}