diff --git a/src/main/java/dev/escalated/services/email/inbound/MailgunInboundParser.java b/src/main/java/dev/escalated/services/email/inbound/MailgunInboundParser.java
new file mode 100644
index 0000000..1ca930d
--- /dev/null
+++ b/src/main/java/dev/escalated/services/email/inbound/MailgunInboundParser.java
@@ -0,0 +1,135 @@
+package dev.escalated.services.email.inbound;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.springframework.stereotype.Component;
+
+/**
+ * Parses Mailgun's inbound webhook payload into an
+ * {@link InboundMessage}. Mailgun POSTs {@code multipart/form-data}
+ * with snake-case field names: {@code sender}, {@code recipient},
+ * {@code subject}, {@code body-plain}, {@code body-html},
+ * {@code Message-Id}, {@code In-Reply-To}, {@code References}, plus
+ * a JSON-encoded {@code attachments} field.
+ *
+ *
The Spring controller already ingests a
+ * {@code Map} from the webhook body, so this parser
+ * just reads from that map.
+ *
+ * Mailgun hosts attachment content behind provider URLs (for
+ * large attachments); we carry the URL through in
+ * {@link InboundAttachment#downloadUrl} so a follow-up worker can
+ * fetch + persist out-of-band.
+ */
+@Component
+public class MailgunInboundParser implements InboundEmailParser {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Override
+ public String name() {
+ return "mailgun";
+ }
+
+ @Override
+ public InboundMessage parse(Object rawPayload) {
+ @SuppressWarnings("unchecked")
+ Map payload = rawPayload instanceof Map, ?> m
+ ? (Map) m
+ : MAPPER.convertValue(rawPayload, new TypeReference