How to Decrypt Outlook 365 S/MIME Emails in Java Using Microsoft Graph API
Decrypting encrypted Outlook 365 emails is not as straightforward as calling a single API endpoint. While Microsoft Graph API allows developers to retrieve email content, S/MIME-encrypted messages require additional cryptographic processing befor...

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.
1. Why “decrypting Outlook 365 emails” is trickier than it sounds
1.1. Pick the encryption type first: S/MIME vs OME (because they behave very differently)
1.2. What Microsoft Graph actually gives you: the MIME message, not the plaintext
/$value segment. This is the critical detail: MIME is where S/MIME encryption and signatures actually live (for example as application/pkcs7-mime). If you retrieve the “nice JSON body,” you might not get what you need for cryptography; if you retrieve MIME, you get the real raw payload that contains the encrypted CMS/PKCS#7 structure. Microsoft documents this $value approach explicitly. (Microsoft Learn)
1.3. The security reality check your future self will thank you for
2. Architecture that works in the real world
2.1. Authentication and permissions: getting to the mailbox without getting blocked
Mail.Read or Mail.ReadBasic are common, but the exact choice depends on whether you’re acting as a user or an application. The important part is that once you have an access token, the MIME download is just an HTTP GET to the /$value endpoint. (Microsoft Learn)
2.2. Getting the MIME content with Graph: the one call you must get right
GET /users/{id}/messages/{id}/$value. That returns the raw RFC 5322 message bytes. This is exactly what you want for S/MIME because it preserves the encrypted part in its original form. (Microsoft Learn)
2.3. Decrypting S/MIME in Java: what “decrypt” actually means
application/pkcs7-mime). Decrypting it means: locate the CMS blob, load the recipient certificate + private key, and ask a CMS library to decrypt the content encryption key and then the content. In Java, this is most commonly done with Bouncy Castle. Once decrypted, you typically get another MIME message (the real body), which you parse again to read the subject/body/attachments.
3. Full Java example: download MIME via Graph, decrypt S/MIME with Bouncy Castle, parse plaintext
import jakarta.mail.Session;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.BodyPart;
import org.bouncycastle.cms.;
import org.bouncycastle.cms.jcajce.;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.;
import java.net.URI;
import java.net.http.;
import java.nio.charset.StandardCharsets;
import java.security.;
import java.security.cert.X509Certificate;
import java.util.Properties;
public class GraphSmimeDecryptor {
static {
Security.addProvider(new BouncyCastleProvider());
}
public static void main(String[] args) throws Exception {
String accessToken = System.getenv("GRAPH_ACCESS_TOKEN");
String userId = "me"; // or a user GUID/email for /users/{id}
String messageId = System.getenv("GRAPH_MESSAGE_ID");
// 1) Download raw MIME from Microsoft Graph
byte[] rawMime = downloadMessageMime(accessToken, userId, messageId);
// 2) Parse the MIME so we can locate the S/MIME encrypted part
MimeMessage outer = parseMime(rawMime);
// 3) Extract CMS/PKCS7 bytes from the message (simple/common S/MIME layout)
byte[] pkcs7 = extractPkcs7Payload(outer);
if (pkcs7 == null) {
throw new IllegalStateException("No S/MIME PKCS#7 payload found. Message may be unencrypted or uses a different layout.");
}
// 4) Load recipient private key + certificate used for S/MIME decryption
KeyMaterial km = loadPkcs12("recipient-keystore.p12", "p12Password".toCharArray());
// 5) Decrypt CMS envelope => yields inner (plaintext) MIME bytes
byte[] innerMimeBytes = decryptCmsEnvelopedData(pkcs7, km.certificate, km.privateKey);
// 6) Parse decrypted MIME and print a readable body
MimeMessage inner = parseMime(innerMimeBytes);
String bodyText = extractBestEffortBodyText(inner);
System.out.println("=== Decrypted Subject ===");
System.out.println(inner.getSubject());
System.out.println(" === Decrypted Body (best effort) ===");
System.out.println(bodyText);
}
// ---------------- Graph download ----------------
static byte[] downloadMessageMime(String accessToken, String userId, String messageId) throws Exception {
String url = "https://graph.microsoft.com/v1.0/" +
(userId.equals("me") ? "me" : "users/" + userId) +
"/messages/" + messageId + "/$value";
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + accessToken)
.header("Accept", "application/octet-stream")
.GET()
.build();
HttpResponse<byte[]> resp = client.send(req, HttpResponse.BodyHandlers.ofByteArray());
if (resp.statusCode() != 200) {
String msg = new String(resp.body(), StandardCharsets.UTF_8);
throw new IOException("Graph returned " + resp.statusCode() + ": " + msg);
}
return resp.body();
}
// ---------------- MIME parsing ----------------
static MimeMessage parseMime(byte[] raw) throws Exception {
Session session = Session.getInstance(new Properties());
try (InputStream is = new ByteArrayInputStream(raw)) {
return new MimeMessage(session, is);
}
}
/**
Common S/MIME encrypted messages look like:
Content-Type: application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m
This method tries a few typical places to find that blob.
/
static byte[] extractPkcs7Payload(MimeMessage msg) throws Exception {
Object content = msg.getContent();
// Case A: the whole message content is already the pkcs7 part
String ct = msg.getContentType().toLowerCase();
if (ct.contains("application/pkcs7-mime") || ct.contains("application/x-pkcs7-mime")) {
return readAllBytes(msg.getInputStream());
}
// Case B: multipart wrapper where one part is pkcs7
if (content instanceof MimeMultipart mm) {
for (int i = 0; i < mm.getCount(); i++) {
BodyPart bp = mm.getBodyPart(i);
String pct = bp.getContentType().toLowerCase();
if (pct.contains("application/pkcs7-mime") || pct.contains("application/x-pkcs7-mime")) {
try (InputStream is = bp.getInputStream()) {
return readAllBytes(is);
}
}
}
}
return null;
}
static byte[] readAllBytes(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int r;
while ((r = is.read(buf)) != -1) {
bos.write(buf, 0, r);
}
return bos.toByteArray();
}
// ---------------- Key loading ----------------
static class KeyMaterial {
final X509Certificate certificate;
final PrivateKey privateKey;
KeyMaterial(X509Certificate cert, PrivateKey pk) { this.certificate = cert; this.privateKey = pk; }
}
static KeyMaterial loadPkcs12(String p12Path, char[] password) throws Exception {
KeyStore ks = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream(p12Path)) {
ks.load(is, password);
}
String alias = ks.aliases().nextElement();
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, password);
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
if (privateKey == null || cert == null) {
throw new IllegalStateException("Could not load key material from PKCS#12 keystore.");
}
return new KeyMaterial(cert, privateKey);
}
// ---------------- CMS decrypt ----------------
static byte[] decryptCmsEnvelopedData(byte[] pkcs7, X509Certificate recipientCert, PrivateKey recipientKey) throws Exception {
CMSEnvelopedData enveloped = new CMSEnvelopedData(pkcs7);
RecipientInformationStore recipients = enveloped.getRecipientInfos();
RecipientId rid = new JceKeyTransRecipientId(recipientCert);
RecipientInformation recipient = recipients.get(rid);
if (recipient == null) {
// If cert matching fails, fall back to first recipient as a last resort
recipient = recipients.getRecipients().stream().findFirst().orElse(null);
}
if (recipient == null) {
throw new IllegalStateException("No recipients found in CMS envelope.");
}
JceKeyTransEnvelopedRecipient r = new JceKeyTransEnvelopedRecipient(recipientKey).setProvider("BC");
return recipient.getContent(r);
}
// ---------------- Body extraction ----------------
static String extractBestEffortBodyText(MimeMessage msg) throws Exception {
Object content = msg.getContent();
if (content instanceof String s) return s;
if (content instanceof MimeMultipart mm) {
// Prefer text/plain if present; otherwise fall back to first text/
String fallback = null;
for (int i = 0; i < mm.getCount(); i++) {
BodyPart bp = mm.getBodyPart(i);
String ct = bp.getContentType().toLowerCase();
if (ct.startsWith("text/plain")) return (String) bp.getContent();
if (fallback == null && ct.startsWith("text/")) fallback = (String) bp.getContent();
}
if (fallback != null) return fallback;
}
return "[No readable text body found]";
}
}
3.1. What the Graph download code is doing, line by line in plain English
downloadMessageMime method calls the Graph endpoint that returns message bytes, not a JSON object. That’s intentional, because S/MIME is a MIME-level encryption mechanism, so you want the raw RFC 5322 content that still contains the PKCS#7 envelope. Microsoft documents this MIME retrieval pattern via the /$value segment, so you’re using the API the way it was designed, not fighting it. (Microsoft Learn)
3.2. Why the MIME parsing step matters before you decrypt
extractPkcs7Payload method is doing the boring but necessary work of locating that part, because real emails vary: sometimes the whole message is the pkcs7 blob, sometimes it’s a multipart wrapper, sometimes clients add extra structure. If you skip this step and just “decrypt the whole message,” you’ll often decrypt the wrong bytes and get garbage.
3.3. How the decryption works without any hand-wavy “crypto magic”
decryptCmsEnvelopedData treats the PKCS#7 bytes as CMS EnvelopedData. Inside that envelope is a list of recipients; each recipient entry contains an encrypted content key intended for a specific certificate. The code tries to match the recipient using your certificate, and if that fails (because life is messy and email clients are creative), it falls back to the first recipient as a last resort. Then Bouncy Castle uses your private key to unwrap the content key and decrypts the payload, giving you the inner plaintext MIME bytes. That inner payload is usually another full email message, which is why you parse MIME again and then extract the human-readable body.
4. Practical problems you’ll hit in production and how to think about them
4.1. Key management: where do you store the private key without creating a disaster
.p12 next to your JAR, congratulations: you’ve invented “encryption theatre.” In production, you typically store private keys in an HSM, a cloud key vault, or at minimum an encrypted secret store with strict access control. The point is to make it harder for an attacker (or an overly-curious teammate) to turn your service into a mailbox reader with benefits.
4.2. Attachments and rich content: the decrypted email can still be complex
4.3. What about sending encrypted emails from Java with Graph
sendMail. (Microsoft Learn)
5. Closing thoughts
5.1. Your turn
Read more at : How to Decrypt Outlook 365 S/MIME Emails in Java Using Microsoft Graph API





