From 6cef3dcd332acc38d56b9a5571231bd295f5d340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Keresztury?= Date: Thu, 18 Jun 2020 11:15:47 +0200 Subject: [PATCH] Initial commit --- .gitignore | 365 +++++++++++++++++++++++++ EventDetails/ClientInfo.cs | 20 ++ EventDetails/DeliveryStatus.cs | 30 ++ EventDetails/Envelope.cs | 18 ++ EventDetails/Flags.cs | 20 ++ EventDetails/Geolocation.cs | 16 ++ EventDetails/Message.cs | 21 ++ EventDetails/MessageDetails/Headers.cs | 18 ++ EventDetails/Reject.cs | 14 + EventDetails/Route.cs | 18 ++ EventDetails/Storage.cs | 14 + LICENCE | 21 ++ Mailgun.Models.SignedEvent.csproj | 22 ++ MailgunEvent.cs | 131 +++++++++ MailgunSignature.cs | 66 +++++ README.md | 55 ++++ SignedEvent.cs | 14 + stylecop.json | 16 ++ 18 files changed, 879 insertions(+) create mode 100644 .gitignore create mode 100644 EventDetails/ClientInfo.cs create mode 100644 EventDetails/DeliveryStatus.cs create mode 100644 EventDetails/Envelope.cs create mode 100644 EventDetails/Flags.cs create mode 100644 EventDetails/Geolocation.cs create mode 100644 EventDetails/Message.cs create mode 100644 EventDetails/MessageDetails/Headers.cs create mode 100644 EventDetails/Reject.cs create mode 100644 EventDetails/Route.cs create mode 100644 EventDetails/Storage.cs create mode 100644 LICENCE create mode 100644 Mailgun.Models.SignedEvent.csproj create mode 100644 MailgunEvent.cs create mode 100644 MailgunSignature.cs create mode 100644 README.md create mode 100644 SignedEvent.cs create mode 100644 stylecop.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e6566f --- /dev/null +++ b/.gitignore @@ -0,0 +1,365 @@ + +# Created by https://www.gitignore.io/api/dotnetcore,visualstudio +# Edit at https://www.gitignore.io/?templates=dotnetcore,visualstudio + +### DotnetCore ### +# .NET Core build folders +/bin +/obj + +# Common node modules locations +/node_modules +/wwwroot/node_modules + + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# End of https://www.gitignore.io/api/dotnetcore,visualstudio diff --git a/EventDetails/ClientInfo.cs b/EventDetails/ClientInfo.cs new file mode 100644 index 0000000..8eb8f61 --- /dev/null +++ b/EventDetails/ClientInfo.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class ClientInfo + { + public string ClientType { get; set; } + + public string ClientOs { get; set; } + + public string DeviceType { get; set; } + + public string ClientName { get; set; } + + public string UserAgent { get; set; } + } +} diff --git a/EventDetails/DeliveryStatus.cs b/EventDetails/DeliveryStatus.cs new file mode 100644 index 0000000..31b6491 --- /dev/null +++ b/EventDetails/DeliveryStatus.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class DeliveryStatus + { + public bool Tls { get; set; } + + public string MxHost { get; set; } + + public string Code { get; set; } + + public string Description { get; set; } + + public float SessionSeconds { get; set; } + + public int RetrySeconds { get; set; } + + public bool Utf8 { get; set; } + + public int AttemptNo { get; set; } + + public string Message { get; set; } + + public bool CertificateVerified { get; set; } + } +} diff --git a/EventDetails/Envelope.cs b/EventDetails/Envelope.cs new file mode 100644 index 0000000..a4d03b0 --- /dev/null +++ b/EventDetails/Envelope.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class Envelope + { + public string Transport { get; set; } + + public string Sender { get; set; } + + public string SendingIp { get; set; } + + public string Targets { get; set; } + } +} diff --git a/EventDetails/Flags.cs b/EventDetails/Flags.cs new file mode 100644 index 0000000..981314f --- /dev/null +++ b/EventDetails/Flags.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class Flags + { + public bool IsRouted { get; set; } + + public bool IsAuthenticated { get; set; } + + public bool IsSystemTest { get; set; } + + public bool IsTestMode { get; set; } + + public bool IsDelayedBounce { get; set; } + } +} diff --git a/EventDetails/Geolocation.cs b/EventDetails/Geolocation.cs new file mode 100644 index 0000000..5dcb1f2 --- /dev/null +++ b/EventDetails/Geolocation.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class Geolocation + { + public string Country { get; set; } + + public string Region { get; set; } + + public string City { get; set; } + } +} diff --git a/EventDetails/Message.cs b/EventDetails/Message.cs new file mode 100644 index 0000000..70f7212 --- /dev/null +++ b/EventDetails/Message.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Mailgun.Models.SignedEvent.EventDetails.MessageDetails; + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class Message + { + public Headers Headers { get; set; } + + public List Attachments { get; set; } + + public List Recipients { get; set; } + + public int Size { get; set; } + } +} diff --git a/EventDetails/MessageDetails/Headers.cs b/EventDetails/MessageDetails/Headers.cs new file mode 100644 index 0000000..a9e7049 --- /dev/null +++ b/EventDetails/MessageDetails/Headers.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent.EventDetails.MessageDetails +{ + public class Headers + { + public string To { get; set; } + + public string MessageId { get; set; } + + public string From { get; set; } + + public string Subject { get; set; } + } +} diff --git a/EventDetails/Reject.cs b/EventDetails/Reject.cs new file mode 100644 index 0000000..c8ef38f --- /dev/null +++ b/EventDetails/Reject.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class Reject + { + public string Reason { get; set; } + + public string Description { get; set; } + } +} diff --git a/EventDetails/Route.cs b/EventDetails/Route.cs new file mode 100644 index 0000000..aede3eb --- /dev/null +++ b/EventDetails/Route.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class Route + { + public string Expression { get; set; } + + public string Id { get; set; } + + public Dictionary Match { get; set; } + } +} diff --git a/EventDetails/Storage.cs b/EventDetails/Storage.cs new file mode 100644 index 0000000..68f9962 --- /dev/null +++ b/EventDetails/Storage.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent.EventDetails +{ + public class Storage + { + public string Url { get; set; } + + public string Key { get; set; } + } +} diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..3247aed --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Balazs Keresztury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Mailgun.Models.SignedEvent.csproj b/Mailgun.Models.SignedEvent.csproj new file mode 100644 index 0000000..b69f5ad --- /dev/null +++ b/Mailgun.Models.SignedEvent.csproj @@ -0,0 +1,22 @@ + + + + netstandard1.6 + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/MailgunEvent.cs b/MailgunEvent.cs new file mode 100644 index 0000000..f30ea6f --- /dev/null +++ b/MailgunEvent.cs @@ -0,0 +1,131 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Mailgun.Models.SignedEvent.EventDetails; + +namespace Mailgun.Models.SignedEvent +{ + /// + /// Mailgun tracks all of the events that occur throughout the system. Below are listed the events that you can retrieve using this API. + /// + public enum Event + { + /// + /// Mailgun accepted the request to send/forward the email and the message has been placed in queue. + /// + Accepted, + + /// + /// Mailgun rejected the request to send/forward the email. + /// + Rejected, + + /// + /// Mailgun sent the email and it was accepted by the recipient email server. + /// + Delivered, + + /// + /// Mailgun could not deliver the email to the recipient email server. + /// severity = permanent when a message is not delivered. There are several reasons why Mailgun stops attempting to deliver messages and drops them including: hard bounces, messages that reached their retry limit, previously unsubscribed/bounced/complained addresses, or addresses rejected by an ESP. + /// severity = temporary when a message is temporary rejected by an ESP. + /// + Failed, + + /// + /// The email recipient opened the email and enabled image viewing. Open tracking must be enabled in the Mailgun control panel, and the CNAME record must be pointing to mailgun.org. + /// + Opened, + + /// + /// The email recipient clicked on a link in the email. Click tracking must be enabled in the Mailgun control panel, and the CNAME record must be pointing to mailgun.org. + /// + Clicked, + + /// + /// The email recipient clicked on the unsubscribe link. Unsubscribe tracking must be enabled in the Mailgun control panel. + /// + Unsubscribed, + + /// + /// The email recipient clicked on the spam complaint button within their email client. Feedback loops enable the notification to be received by Mailgun. + /// + Complained, + + /// + /// Mailgun has stored an incoming message + /// + Stored, + } + + public enum LogLevel + { + Info, + Warn, + Error, + } + + /// + /// Events are represented as loosely structured JSON documents. The exact event structure depends on the event type. + /// + public class MailgunEvent + { + /// + /// Gets or sets the type of the event. Events of a particular type have an identical structure, though some fields may be optional. + /// + public Event Event { get; set; } + + /// + /// Gets or sets the event ID. It is guaranteed to be unique within a day. It can be used to distinguish events that have already been retrieved when requests with overlapping time ranges are made. + /// + public string Id { get; set; } + + /// + /// Gets or sets the time the event was generated in the system provided as Unix epoch seconds. + /// + public double Timestamp { get; set; } + + public LogLevel LogLevel { get; set; } + + public string Severity { get; set; } + + public string Reason { get; set; } + + public string Method { get; set; } + + public List Routes { get; set; } + + public Envelope Envelope { get; set; } + + public Flags Flags { get; set; } + + public DeliveryStatus DeliveryStatus { get; set; } + + public Message Message { get; set; } + + public Storage Storage { get; set; } + + public string Recipient { get; set; } + + public string RecipientDomain { get; set; } + + public List Campaigns { get; set; } + + public List Tags { get; set; } + + public Dictionary UserVariables { get; set; } + + public Geolocation Geolocation { get; set; } + + public string Ip { get; set; } + + public ClientInfo ClientInfo { get; set; } + + public string Url { get; set; } + + public Reject Reject { get; set; } + } +} diff --git a/MailgunSignature.cs b/MailgunSignature.cs new file mode 100644 index 0000000..9522421 --- /dev/null +++ b/MailgunSignature.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Mailgun.Models.SignedEvent +{ + public class MailgunSignature + { + public string Timestamp { get; set; } + + public string Token { get; set; } + + public string Signature { get; set; } + + /// + /// Checks if the signature is valid based on the provided API key. + /// https://documentation.mailgun.com/en/latest/user_manual.html#webhooks. + /// + /// mailgun API key. + /// Maximum acceptable time difference between the creation of the signature and now. + /// True if the signature is valid. + public bool IsValid(string apiKey, TimeSpan tolerance) + { + // parse timestamp as a DateTime object + var timestamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToDouble(this.Timestamp)); + if (DateTime.UtcNow - timestamp > tolerance) + { + // if the signature was made too long ago, return false + return false; + } + + // concatenate timestamp and token then sign it with the API key + var hasher = new HMACSHA256() + { + Key = Encoding.ASCII.GetBytes(apiKey), + }; + var calculated_signature = hasher.ComputeHash(Encoding.ASCII.GetBytes(this.Timestamp + this.Token)); + + // convert calculated signature to hexdigest format + string hash_hex = string.Empty; + foreach (var b in calculated_signature) + { + hash_hex += string.Format("{0:x2}", b); + } + + // compare + return this.Signature.Equals(hash_hex); + } + + /// + /// Checks if the signature is valid based on the provided API key with a maximum allowed time skew of 10 minutes. + /// https://documentation.mailgun.com/en/latest/user_manual.html#webhooks. + /// + /// malgun API key. + /// True if the signature is valid. + public bool IsValid(string apiKey) + { + return this.IsValid(apiKey, new TimeSpan(0, 10, 0)); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..08c8a20 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Mailgun.Models.SignedEvent for Mailgun webhooks + +Implements the data model for Mailgun events to use with custom webhooks. + +This library can be used as a data model for the deserialization of an incoming event data with any JSON serializer of your choice. It even provides a handy function to [verify its cryptographic signature](https://documentation.mailgun.com/en/latest/user_manual.html#webhooks). + +Since it targets [.NET Standard 1.6](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) it is compatible with a wide variety of platforms (such as .NET Framework 4.6.1, .NET Core 1.0 and up). + +## Background + +Mailgun provides *developer friendly* transactional e-mail service. In spite of this claim there's still no official SDK for nearly any platform and their documentation often lacks basic information. However it's still one of the best options you have if you don't want to implement your own e-mail delivery service which can become very complex very quickly. + +Once you have an account with them you can [subscribe](https://documentation.mailgun.com/en/latest/user_manual.html#webhooks) to various messaging events so when the appropriate event happens (eg. the e-mail was delivered or bounced) Mailgun will POST a JSON encoded object to the URL you provided. + +This library was created to ease the burden of deserializing these events into POCOs and to provide an easy way to verify the cryptographic signature of an incoming packet. + +## Usage + +Here's a practical example using ASP.NET Core 3.1: + +```csharp +[Route("[controller]")] +[ApiController] +public class DeliveredController : ControllerBase +{ + [HttpPost] + public async Task> PostDelivered([FromBody] SignedEvent signedEvent) + { + if (!signedEvent.Signature.IsValid("your-api-key")) + { + // if the signature is invalid, return 401 + return Unauthorized(); + } + + // do something meaningful with signedEvent.Event here + + // finally return 201 so Mailgun knows POST has been successful. Otherwise it'll keep retrying + return CreatedAtAction(nameof(PostDelivered), null); + } +} +``` + +A `SignedEvent` contains a `Signature` and the actual `Event`. While not mandatory it's recommended to check the signature to make sure it was actually signed by Mailgun. + +Since the signature is created using the actual timestamp you can specify how old a signature can be to still considered as valid. It is set to 10 minutes by default. + +## Important Notes +### Unusual JSON Naming Convention + +Mailgun sends JSON data using an unusual naming convention with dashes between words. This means that the verb `is valid` will be encoded as `is-valid` even though the convention is to encode names as *camelCase* (resulting in `isValid`). + +There are ways to configure most JSON serializers to handle this, but this topic is out of scope of this project. + +### Structure Is Constantly Changing +Please note that these events [can apparently change their structure](https://documentation.mailgun.com/en/latest/api-events.html#event-structure), so don't be surprised if the data you've received doesn't fully correspond to their documentation. If you discover a change (which can only be an addition according to their promise) you're welcome to open a PR. diff --git a/SignedEvent.cs b/SignedEvent.cs new file mode 100644 index 0000000..bc10179 --- /dev/null +++ b/SignedEvent.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Balazs Keresztury. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Mailgun.Models.SignedEvent +{ + public class SignedEvent + { + public MailgunSignature Signature { get; set; } + + public MailgunEvent EventData { get; set; } + } +} diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 0000000..3f0420f --- /dev/null +++ b/stylecop.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Balazs Keresztury", + "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.", + "variables": { + "licenseName": "MIT", + "licenseFile": "LICENSE" + } + }, + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace" + } + } +}