Skip to main content

messageComplaint

The messageComplaint webhook event is triggered when EmailEngine detects a feedback loop (FBL) complaint in a monitored mailbox. This event helps you identify when recipients have marked your emails as spam or unwanted, allowing you to maintain sender reputation and comply with email best practices.

When This Event is Triggered​

The messageComplaint event fires when a message arriving in the Inbox is recognized as an abuse report and the report names at least one complaining recipient (Original-Rcpt-To, or the Hotmail X-HmXmrOriginalRecipient header). A report without a recipient produces no event. The check runs on every account type: IMAP, Gmail API and MS Graph.

A message is checked for complaint content when one of these holds:

  • It has a message/feedback-report part (an ARF report, RFC 5965)
  • It comes from staff@hotmail.com, embeds the original message as message/rfc822 or message/rfc822-headers, and has complaint in the subject (Microsoft JMRP reports)

Messages in other folders are not checked.

The complaint message itself also produces a messageNew event, sent before this one, with isComplaint: true and relatedMessageId set to the Message-ID of the reported message when the report included it. On IMAP accounts neither event is sent for messages dated before the account's notifyFrom.

Payload Schema​

Top-Level Fields​

FieldTypeRequiredDescription
serviceUrlstring or nullYesThe configured EmailEngine service URL, null if not set
accountstringYesAccount ID that received the complaint message
datestringYesISO 8601 timestamp when the webhook was generated
eventstringYesAlways messageComplaint
dataobjectYesComplaint data object (see below)

The event carries no path or specialUse. The unique event identifier is sent as the HTTP header X-EE-Wh-Event-Id, not in the JSON payload.

Complaint Data Fields (data object)​

FieldTypeRequiredDescription
complaintMessagestringYesEmailEngine message ID of the complaint notification email itself. Fetch it through the message API to read the full report
arfobjectYesFields of the feedback report, camelCased (see below)
headersobjectNoSelected headers of the reported message, when the report embedded it (see below)

ARF Object Structure​

Every field of the message/feedback-report part is passed through under its camelCased name, so the exact set depends on the reporter. These fields are single strings:

FieldDescription
feedbackTypeType of report as stated by the reporter, typically abuse
userAgentSoftware that generated the report
versionARF format version, typically 1
originalEnvelopeIdEnvelope ID of the original delivery
originalMailFromEnvelope sender of the original message, without angle brackets
abuseTypeSpecific abuse type, for example complaint
arrivalDateWhen the original message arrived, as an ISO 8601 timestamp. Received-Date is reported under this name as well
reportingMtaMTA that generated the report, as written in the report (for example dns; mx.isp.example.com)
sourceIpIP address the original message was sent from
sourceName of the reporting system
subscriptionLinkLink to subscription preferences
incidentsNumber of incidents the report covers

Any other field, including originalRcptTo, is an array of strings with one entry per occurrence. originalRcptTo lists the recipients who complained and is the field that decides whether the event is sent at all. Empty fields are omitted, and an Arrival-Date that does not parse is dropped.

For Microsoft JMRP reports EmailEngine fills in what the report leaves out: source is set to Hotmail, feedbackType to abuse and abuseType to complaint unless the report says otherwise, and originalMailFrom falls back to the Return-Path of the embedded message.

Headers Object Structure​

When the report embeds the reported message (message/rfc822, message/rfc822-headers, text/rfc822-headers or text/rfc822-header), headers carries these values from it. Fields the embedded message does not have are omitted, and the object is absent when nothing was found:

FieldTypeDescription
messageIdstringMessage-ID of the reported message
fromstringSender address
toarrayRecipient addresses
ccarrayCC addresses
bccarrayBCC addresses
subjectstringSubject line, MIME decoded
datestringDate header as an ISO 8601 timestamp

Example Payload​

{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T07:06:11.697Z",
"event": "messageComplaint",
"data": {
"complaintMessage": "AAAAAQAABzE",
"arf": {
"source": "Hotmail",
"feedbackType": "abuse",
"abuseType": "complaint",
"originalMailFrom": "sender@example.com",
"originalRcptTo": ["recipient@hotmail.com"],
"sourceIp": "203.0.113.42",
"arrivalDate": "2025-10-17T07:06:35.021Z"
},
"headers": {
"messageId": "<57f34982-43cc-6534-40f9-0f72f1c8a158@example.com>",
"from": "sender@example.com",
"to": ["recipient@hotmail.com"],
"subject": "Your weekly newsletter",
"date": "2025-10-17T07:06:34.000Z"
}
}
}

ARF Parsing Details​

EmailEngine parses Abuse Reporting Format (ARF) messages according to RFC 5965 and the Microsoft JMRP variant.

Parsed Content Types​

EmailEngine examines message attachments for these content types:

Content-TypePurpose
message/feedback-reportStandard ARF feedback report with structured fields
message/rfc822Complete original message; only its headers are read
message/rfc822-headersOriginal message headers only
text/rfc822-headersOriginal message headers (alternate type)
text/rfc822-headerOriginal message headers (singular form)

Every one of these parts is downloaded and read. Before v2.81.2 only the first three were, so a report that put the original headers in a text/rfc822-headers or text/rfc822-header part had no headers object and produced the event only when the feedback report itself named a recipient.

Extracted Original Message Headers​

From the embedded original message, EmailEngine reads:

HeaderWebhook FieldDescription
Message-IDheaders.messageIdMessage-ID of the reported message
Fromheaders.fromSender address
Toheaders.toRecipient addresses (array)
CCheaders.ccCC addresses (array)
BCCheaders.bccBCC addresses (array)
Subjectheaders.subjectSubject line (MIME decoded)
Dateheaders.dateMessage date (ISO 8601)
Return-Patharf.originalMailFromEnvelope sender, JMRP reports only, when the report has no Original-Mail-From
X-HmXmrOriginalRecipientarf.originalRcptToOriginal recipient (Hotmail), appended to the list
X-Sender-IParf.sourceIpSender IP (Microsoft), when the report has no Source-IP
X-MS-Exchange-CrossTenant-OriginalArrivalTimearf.arrivalDateArrival time (Microsoft), when the report has no Arrival-Date

Example: Standard ARF Message​

A standard RFC 5965 complaint message looks like this:

From: abusedesk@isp.example.com
To: abuse@sender.example.com
Subject: FBL Notification
Content-Type: multipart/report; report-type=feedback-report; boundary=boundary

--boundary
Content-Type: text/plain

This is a spam complaint from one of our users.

--boundary
Content-Type: message/feedback-report

Feedback-Type: abuse
User-Agent: ISP-FBL/1.0
Version: 1
Original-Mail-From: newsletter@sender.example.com
Original-Rcpt-To: user@isp.example.com
Arrival-Date: Thu, 17 Oct 2025 07:06:35 +0000
Source-IP: 203.0.113.42
Reporting-MTA: dns; mx.isp.example.com

--boundary
Content-Type: message/rfc822

From: newsletter@sender.example.com
To: user@isp.example.com
Subject: Your Weekly Newsletter
Date: Thu, 17 Oct 2025 07:06:34 +0000
Message-ID: <abc123@sender.example.com>

The original message body.

--boundary--

EmailEngine parses this into:

{
"arf": {
"feedbackType": "abuse",
"userAgent": "ISP-FBL/1.0",
"version": "1",
"originalMailFrom": "newsletter@sender.example.com",
"originalRcptTo": ["user@isp.example.com"],
"arrivalDate": "2025-10-17T07:06:35.000Z",
"sourceIp": "203.0.113.42",
"reportingMta": "dns; mx.isp.example.com"
},
"headers": {
"messageId": "<abc123@sender.example.com>",
"from": "newsletter@sender.example.com",
"to": ["user@isp.example.com"],
"subject": "Your Weekly Newsletter",
"date": "2025-10-17T07:06:34.000Z"
}
}

Example: Hotmail/JMRP Complaint​

Microsoft's JMRP complaints have no feedback report part. The original recipient and the sending IP are in headers of the embedded message:

From: staff@hotmail.com
To: abuse@sender.example.com
Subject: complaint about message from 203.0.113.42
Content-Type: multipart/report; report-type=feedback-report; boundary=boundary

--boundary
Content-Type: text/plain

This is a complaint from a Hotmail user.

--boundary
Content-Type: message/rfc822-headers

Return-Path: <newsletter@sender.example.com>
From: newsletter@sender.example.com
To: user@hotmail.com
Subject: Your Weekly Newsletter
Date: Thu, 17 Oct 2025 07:06:34 +0000
Message-ID: <abc123@sender.example.com>
X-HmXmrOriginalRecipient: user@hotmail.com
X-Sender-IP: 203.0.113.42
X-MS-Exchange-CrossTenant-OriginalArrivalTime: 17 Oct 2025 07:06:35.0210 (UTC)

--boundary--

EmailEngine recognizes the staff@hotmail.com sender and reports:

{
"arf": {
"source": "Hotmail",
"feedbackType": "abuse",
"abuseType": "complaint",
"originalMailFrom": "newsletter@sender.example.com",
"originalRcptTo": ["user@hotmail.com"],
"sourceIp": "203.0.113.42",
"arrivalDate": "2025-10-17T07:06:35.021Z"
},
"headers": {
"messageId": "<abc123@sender.example.com>",
"from": "newsletter@sender.example.com",
"to": ["user@hotmail.com"],
"subject": "Your Weekly Newsletter",
"date": "2025-10-17T07:06:34.000Z"
}
}

Handling Incomplete ARF Data​

Not every report carries the full ARF field set. EmailEngine extracts whatever is present, so arf.source, arf.feedbackType, arf.originalMailFrom and the headers object can each be absent; only a complaining recipient is guaranteed, because a report without one produces no event.

Understanding FBL Complaints​

Recipients mark emails as spam for various reasons:

  • Unwanted emails - User no longer wants to receive messages
  • Forgotten subscription - User doesn't remember signing up
  • Difficult unsubscribe - Easier to click "spam" than find unsubscribe link
  • Misleading content - Email doesn't match user expectations
  • Excessive frequency - Too many emails sent too often

Mailbox providers deliver these reports through feedback loop programs that a sender enrolls in, such as Microsoft's Junk Mail Reporting Program (JMRP) for Outlook.com and Hotmail. The reports arrive at the address registered with the program, so that mailbox has to be one EmailEngine monitors.

Handling the Event​

The complaining recipients are in arf.originalRcptTo, and headers.messageId names the reported message when the report included its headers. Suppress every complainant at once, and keep the Message-ID so the complaint can be tied back to the sent message:

async function handleMessageComplaint(event) {
const { account, data, date } = event;
const { arf, headers } = data;

for (const email of arf.originalRcptTo) {
await db.suppressionList.upsert({
where: { email: email.toLowerCase() },
create: {
email: email.toLowerCase(),
reason: 'complaint',
source: arf.source || null,
account,
originalMessageId: headers?.messageId || null,
createdAt: new Date(date)
},
update: {}
});
}
}
  • messageBounce - Triggered when a bounce notification is received
  • messageFailed - Triggered when EmailEngine gives up on a queued email
  • messageSent - Triggered when a message is accepted for delivery
  • messageNew - The complaint notification also triggers this event

See Also​