@@ -8,25 +8,46 @@ namespace Smtp2Go.NET.Models.Webhooks;
88/// <remarks>
99/// <para>
1010/// SMTP2GO sends HTTP POST requests to registered webhook URLs when email
11- /// events occur. This model deserializes the inbound webhook payload.
11+ /// events occur. This model is the canonical in-memory representation of
12+ /// JSON and form-encoded webhook callbacks.
1213/// </para>
1314/// <para>
14- /// The fields populated depend on the event type:
15+ /// Live callbacks captured in the integration test suite showed that SMTP2GO does not emit one
16+ /// stable callback shape. The fields populated depend on the event type:
1517/// <list type="bullet">
1618/// <item><see cref="Recipient"/> (<c>rcpt</c>) is present for delivered and bounce events.</item>
1719/// <item><see cref="Recipients"/> is present for processed events (array of all recipients).</item>
1820/// <item><see cref="BounceType"/>, <see cref="BounceContext"/>, and <see cref="Host"/>
1921/// are present for bounce and delivered events.</item>
2022/// <item><see cref="ClickUrl"/> and <see cref="Link"/> are only present for click events.</item>
23+ /// <item>
24+ /// Processed and delivered callbacks include additional provider metadata such as
25+ /// <c>id</c>, <c>auth</c>, <c>message-id</c>/<c>Message-Id</c>, <c>subject</c>/<c>Subject</c>,
26+ /// <c>from</c>, <c>from_address</c>, and <c>from_name</c>.
27+ /// </item>
2128/// </list>
2229/// </para>
2330/// </remarks>
2431/// <example>
2532/// <code>
2633/// // In an ASP.NET Core controller:
2734/// [HttpPost("webhooks/smtp2go")]
28- /// public IActionResult HandleWebhook([FromBody] WebhookCallbackPayload payload )
35+ /// public async Task< IActionResult> HandleWebhook(CancellationToken cancellationToken )
2936/// {
37+ /// WebhookCallbackPayload payload;
38+ ///
39+ /// if (Request.HasFormContentType)
40+ /// {
41+ /// var form = await Request.ReadFormAsync(cancellationToken);
42+ /// payload = WebhookCallbackPayloadParser.ParseFormValues(
43+ /// form.SelectMany(pair => pair.Value.Select(value => new KeyValuePair<string, string?>(pair.Key, value))));
44+ /// }
45+ /// else
46+ /// {
47+ /// payload = await Request.ReadFromJsonAsync<WebhookCallbackPayload>(cancellationToken: cancellationToken)
48+ /// ?? new WebhookCallbackPayload();
49+ /// }
50+ ///
3051/// switch (payload.Event)
3152/// {
3253/// case WebhookCallbackEvent.Delivered:
@@ -64,6 +85,22 @@ public class WebhookCallbackPayload
6485 [ JsonPropertyName ( "email_id" ) ]
6586 public string ? EmailId { get ; init ; }
6687
88+ /// <summary>
89+ /// Gets the SMTP message identifier observed in the webhook callback.
90+ /// </summary>
91+ /// <remarks>
92+ /// <para>
93+ /// This is distinct from <see cref="EmailId" />. <see cref="EmailId" /> is SMTP2GO's provider-side
94+ /// correlation ID returned by the send API, while this property carries the RFC 5322 Message-ID style
95+ /// value observed in webhook callbacks.
96+ /// </para>
97+ /// <para>
98+ /// Live callbacks emitted this field with inconsistent casing (<c>Message-Id</c> and <c>message-id</c>).
99+ /// </para>
100+ /// </remarks>
101+ [ JsonPropertyName ( "message-id" ) ]
102+ public string ? MessageId { get ; init ; }
103+
67104 /// <summary>
68105 /// Gets the type of event that triggered this webhook callback.
69106 /// </summary>
@@ -97,6 +134,35 @@ public class WebhookCallbackPayload
97134 [ JsonPropertyName ( "sendtime" ) ]
98135 public DateTimeOffset ? SendTime { get ; init ; }
99136
137+ /// <summary>
138+ /// Gets the message subject observed in the webhook callback.
139+ /// </summary>
140+ /// <remarks>
141+ /// Live callbacks emitted this field with inconsistent casing (<c>Subject</c> and <c>subject</c>).
142+ /// </remarks>
143+ [ JsonPropertyName ( "subject" ) ]
144+ public string ? Subject { get ; init ; }
145+
146+ /// <summary>
147+ /// Gets the provider-specific callback event identifier.
148+ /// </summary>
149+ /// <remarks>
150+ /// This maps to the raw <c>id</c> field observed in live callbacks. Different events for the same
151+ /// <see cref="EmailId" /> can carry different values, so this is treated as an event-level identifier.
152+ /// </remarks>
153+ [ JsonPropertyName ( "id" ) ]
154+ public string ? EventId { get ; init ; }
155+
156+ /// <summary>
157+ /// Gets the opaque provider auth marker observed in live callbacks.
158+ /// </summary>
159+ /// <remarks>
160+ /// The exact semantics are undocumented. Live callbacks included values such as a truncated API key
161+ /// prefix, so the library preserves the field as opaque diagnostic metadata.
162+ /// </remarks>
163+ [ JsonPropertyName ( "auth" ) ]
164+ public string ? Auth { get ; init ; }
165+
100166 /// <summary>
101167 /// Gets the per-event recipient email address.
102168 /// </summary>
@@ -116,6 +182,29 @@ public class WebhookCallbackPayload
116182 [ JsonPropertyName ( "sender" ) ]
117183 public string ? Sender { get ; init ; }
118184
185+ /// <summary>
186+ /// Gets the raw <c>from</c> field observed in live callbacks.
187+ /// </summary>
188+ /// <remarks>
189+ /// Live callbacks included <c>sender</c>, <c>from</c>, and <c>from_address</c>. They carried the same
190+ /// address in the captured delivered and processed payloads, so this property is preserved separately
191+ /// to avoid losing transport detail.
192+ /// </remarks>
193+ [ JsonPropertyName ( "from" ) ]
194+ public string ? From { get ; init ; }
195+
196+ /// <summary>
197+ /// Gets the raw <c>from_address</c> field observed in live callbacks.
198+ /// </summary>
199+ [ JsonPropertyName ( "from_address" ) ]
200+ public string ? FromAddress { get ; init ; }
201+
202+ /// <summary>
203+ /// Gets the raw <c>from_name</c> field observed in live callbacks.
204+ /// </summary>
205+ [ JsonPropertyName ( "from_name" ) ]
206+ public string ? FromName { get ; init ; }
207+
119208 /// <summary>
120209 /// Gets the list of all recipients of the original email.
121210 /// </summary>
0 commit comments