Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions examples/mail/FileAttachmentExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import com.sendgrid.Method;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.SendGrid;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Attachments;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import java.io.IOException;

public class FileAttachmentExample {

// Small 1x1 PNG image embedded directly; no local image file is required.
private static final String IMAGE_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mOQanoEAAI4AX++PQw4AAAAAElFTkSuQmCC";

public static void main(String[] args) throws IOException {
attachment();
}

public static void attachment() throws IOException {
String apiKey = System.getenv("SENDGRID_API_KEY");
if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IllegalStateException(
"Set the SENDGRID_API_KEY environment variable.");
}

String fromAddress = "sender@example.com";
String toAddress = "recipient@example.com";

Mail mail = new Mail(
new Email(fromAddress),
"SendGrid Java: File attachment example",
new Email(toAddress),
new Content("text/plain",
"Please find the attached image."));

Attachments attachment = new Attachments();
attachment.setContent(IMAGE_BASE64);
attachment.setType("image/png");
attachment.setFilename("mail.png");
attachment.setDisposition("attachment");
mail.addAttachments(attachment);

sendEmail(apiKey, mail);
}

private static void sendEmail(String apiKey, Mail mail)
throws IOException {
Request request = new Request();
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());

Response response = new SendGrid(apiKey).api(request);
System.out.println("HTTP status: " + response.getStatusCode());

if (response.getStatusCode() < 200
|| response.getStatusCode() >= 300) {
System.err.println(response.getBody());
throw new IllegalStateException("Email sending failed.");
}
}
}
68 changes: 68 additions & 0 deletions examples/mail/InlineFileAttachmentExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import com.sendgrid.Method;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.SendGrid;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Attachments;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import java.io.IOException;

public class InlineFileAttachmentExample {

private static final String CONTENT_ID = "company-logo";

// Small 1x1 PNG image embedded directly; no local image file is required.
private static final String IMAGE_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mOQanoEAAI4AX++PQw4AAAAAElFTkSuQmCC";

public static void main(String[] args) throws IOException {
inline();
}

private static void inline() throws IOException {
String apiKey = System.getenv("SENDGRID_API_KEY");
if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IllegalStateException(
"Set the SENDGRID_API_KEY environment variable.");
}

String fromAddress = "sender@example.com";
String toAddress = "recipient@example.com";

String html = "<!doctype html>"
+ "<html>"
+ "<body>"
+ "<h1>Welcome!</h1>"
+ "<p>This image is embedded in the email:</p>"
+ "<img src=\"cid:company-logo\" alt=\"Company logo\" width=\"96\" />"
+ "</body>"
+ "</html>";

Mail mail = new Mail(
new Email(fromAddress),
"SendGrid Java: Inline image example",
new Email(toAddress),
new Content("text/html", html)
);

Attachments inlineImage = new Attachments();
inlineImage.setContent(IMAGE_BASE64);
inlineImage.setType("image/png");
inlineImage.setFilename("mail.png");
inlineImage.setDisposition("inline");
inlineImage.setContentId(CONTENT_ID);
mail.addAttachments(inlineImage);

Request request = new Request();
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());

Response response = new SendGrid(apiKey).api(request);
System.out.println("HTTP status: " + response.getStatusCode());
if (response.getStatusCode() < 200 || response.getStatusCode() > 299) {
System.err.println(response.getBody());
}
}
}
64 changes: 64 additions & 0 deletions examples/mail/PreventThreadingExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import com.sendgrid.Method;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.SendGrid;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import java.io.IOException;
import java.util.UUID;

public class PreventThreadingExample {
public static void main(String[] args) throws IOException {
String apiKey = System.getenv("SENDGRID_API_KEY");
if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IllegalStateException(
"Set the SENDGRID_API_KEY environment variable.");
}

String fromAddress = "sender@example.com";
String toAddress = "recipient@example.com";

// Gmail groups emails that share a subject into one conversation.
// A unique X-Entity-Ref-ID header on each email keeps them separate.
// See https://docs.customer.io/messaging/channels/email/headers/custom-mail-headers/
for (int i = 1; i <= 3; i++) {
Mail mail = new Mail(
new Email(fromAddress),
"Invoice Reminder",
new Email(toAddress),
new Content("text/plain",
"This is email #" + i + ". It should appear as its own conversation."));
mail.addHeader("X-Entity-Ref-ID", UUID.randomUUID().toString());

String sgMessageId = sendEmail(apiKey, mail);
System.out.println("Email #" + i + " sent: " + sgMessageId);
}

System.out.println("All emails sent with unique X-Entity-Ref-ID headers.");
System.out.println("Each should appear as a separate conversation in Gmail.");
}

// Returns the X-Message-Id SendGrid assigned to the email.
private static String sendEmail(String apiKey, Mail mail) throws IOException {
Request request = new Request();
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());

Response response = new SendGrid(apiKey).api(request);
System.out.println("HTTP status: " + response.getStatusCode());

if (response.getStatusCode() < 200
|| response.getStatusCode() >= 300) {
System.err.println(response.getBody());
throw new IllegalStateException("Email sending failed.");
}

String messageId = response.getHeaders().get("X-Message-Id");
if (messageId == null) {
throw new IllegalStateException("SendGrid did not return X-Message-Id.");
}
return messageId;
}
}
71 changes: 71 additions & 0 deletions examples/mail/ThreadingExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import com.sendgrid.Method;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.SendGrid;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import java.io.IOException;

public class ThreadingExample {
public static void main(String[] args) throws IOException {
String apiKey = System.getenv("SENDGRID_API_KEY");
if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IllegalStateException(
"Set the SENDGRID_API_KEY environment variable.");
}

String fromAddress = "sender@example.com";
String toAddress = "recipient@example.com";

if (args.length == 0) {
// Step 1: send the first email. Its smtp-id arrives in the "delivered"
// webhook event, not in this response.
Mail first = new Mail(
new Email(fromAddress),
"SendGrid Java: Threading example",
new Email(toAddress),
new Content("text/plain", "This starts the thread."));
String sgMessageId = sendEmail(apiKey, first);
System.out.println("X-Message-Id: " + sgMessageId);
System.out.println("Find the smtp-id for this message in its delivered "
+ "webhook event, then run again with it as the argument.");
return;
}

// Step 2: reply using the smtp-id from the delivered webhook event,
// for example "<abc123@example-ismtpd-10>".
String smtpId = args[0];
Mail reply = new Mail(
new Email(fromAddress),
"Re: SendGrid Java: Threading example",
new Email(toAddress),
new Content("text/plain", "This reply should group with the first email."));
reply.addHeader("In-Reply-To", smtpId);
reply.addHeader("References", smtpId);
sendEmail(apiKey, reply);
}

// Returns the X-Message-Id SendGrid assigned to the email.
private static String sendEmail(String apiKey, Mail mail) throws IOException {
Request request = new Request();
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());

Response response = new SendGrid(apiKey).api(request);
System.out.println("HTTP status: " + response.getStatusCode());

if (response.getStatusCode() < 200
|| response.getStatusCode() >= 300) {
System.err.println(response.getBody());
throw new IllegalStateException("Email sending failed.");
}

String messageId = response.getHeaders().get("X-Message-Id");
if (messageId == null) {
throw new IllegalStateException("SendGrid did not return X-Message-Id.");
}
return messageId;
}
}
1 change: 1 addition & 0 deletions use-cases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Please [open an issue](https://github.com/sendgrid/sendgrid-ruby/issues) or [mak
* [Transactional Templates](transactional-templates.md)
* [Legacy Templates](legacy-templates.md)
* [Personalizations](personalizations.md)
* [Threaded Replies](threaded-replies.md)

# Twilio Use Cases
* [Twilio Setup](twilio-setup.md)
Expand Down
105 changes: 105 additions & 0 deletions use-cases/threaded-replies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Threaded Replies

This example goes over how to send a reply that stays in the same email thread as an earlier message. You can find documentation for the Event Webhook [here](https://sendgrid.com/docs/API_Reference/Webhooks/event.html), for the Webhooks API [here](https://www.twilio.com/docs/sendgrid/api-reference/webhooks), and for Mail Send [here](https://docs.sendgrid.com/api-reference/mail-send/mail-send).

A reply sent without threading headers shows up in Gmail as a new conversation. For example, a follow-up to an order confirmation lands as a separate message instead of under the original. Setting `In-Reply-To` and `References` to the original email's `smtp-id` keeps the reply in the same thread in Gmail, Outlook, and Apple Mail.

A complete two-step version is in [ThreadingExample.java](../examples/mail/ThreadingExample.java).

## Which ID to use

**Short answer:** reply with the `smtp-id` from the `delivered` webhook event.
The `X-Message-Id` from the send response is not the value that threads a
reply.

Mail clients group messages by the RFC 5322 `Message-ID` header. The webhook's
`smtp-id` is that `Message-ID`, so it's the value to use.

The reply's subject should be the original subject with `Re: ` in front. Gmail uses the subject as a second signal when grouping, so a mismatched subject is more likely to start a new conversation.

## Sequence

```
your app SendGrid webhook endpoint
| POST /mail/send | |
|---------------------------->| |
| response: X-Message-Id | |
|<----------------------------| |
| store X-Message-Id | |
| | delivered (smtp-id) |
| |----------------------------->|
| | | match on sg_message_id,
| | | store smtp-id
| POST /mail/send | |
| In-Reply-To: smtp-id | |
|---------------------------->| |
```

## Matching a webhook event to a send

`sg_message_id` in an event is the response's `X-Message-Id` followed by a
suffix. To match, take the part before the first `.` and compare it with the
stored `X-Message-Id`:

```
sg_message_id: abc123XYZ.recvd-example-1-0
└──────┘
match against stored X-Message-Id: abc123XYZ
```

When it matches, save the event's `smtp-id` on the stored record. This rule is
based on one sample, so confirm it on several real sends before you depend on
it.

Example `delivered` event (trimmed):

```json
{
"email": "recipient@example.com",
"event": "delivered",
"sg_event_id": "ZXhhbXBsZS1ldmVudC1pZA",
"sg_message_id": "abc123XYZ.recvd-example-1-0",
"smtp-id": "<abc123XYZ@example-ismtpd-10>",
"timestamp": 1700000000
}
```

The `processed` event carries the same `smtp-id`, so it also works if you need
the ID before delivery.

## Troubleshooting

- **The reply starts a new thread.** Check which value you sent. The
`X-Message-Id` will not thread. Send the `smtp-id`, and keep its angle
brackets.
- **The reply is in a separate conversation.** Check that the subject is the original subject with `Re: ` in front, as well as the `smtp-id`.
- **There is no `smtp-id` yet.** The reply has to wait for the `delivered` or
`processed` event. Don't send it from the API response alone.
- **No event matches the stored ID.** Compare the part of `sg_message_id` before
the first `.` with the stored `X-Message-Id`, character for character.
- **The same event arrives twice.** Webhooks can repeat, so make the save
idempotent: writing the same `smtp-id` twice should change nothing.

## Verify the thread in Gmail

Check the headers after the reply arrives. This shows whether the reply carries the right values.

1. Open the original email in Gmail and confirm the reply appears under it, in the same conversation.
2. On the reply, click the three-dot **More** menu next to the reply button, then choose **Show original**.
3. In the raw headers, find these lines:
- `Message-ID:` on the original email. It should match the `smtp-id` from the `delivered` event, including the angle brackets.
- `In-Reply-To:` on the reply. It should contain the same `smtp-id`.
- `References:` on the reply. It should contain the same `smtp-id`.
4. If the reply shows in a separate conversation, compare its `In-Reply-To` and `References` values with the original's `Message-ID`. A mismatch means the reply was sent with the wrong ID.

## Viewing SendGrid deliverability webhook events

The Event Webhook needs a public URL to post to. For a first test, use a temporary endpoint:

1. Open [webhook.site](https://webhook.site) and copy the unique URL it gives you.
2. In the SendGrid dashboard, open the Event Webhook settings, paste the URL, and enable the `delivered` and `processed` events.
3. Run `ThreadingExample` to send the first email. Refresh webhook.site and look for the `delivered` event, then check its `sg_message_id` and `smtp-id`.

Other tools that work the same way include [Beeceptor](https://beeceptor.com), which can also mock responses, and the [Svix webhook testing guide](https://www.svix.com/resources/faq/best-webhook-testing-inspecting-and-debugging-tools-2026/), which compares several options.

Temporary endpoints are public, so anyone with the URL can read what is posted to it. Don't send real customer data through one.
Loading