diff --git a/examples/inbound-webhook-handler/.dockerignore b/examples/inbound-webhook-handler/.dockerignore new file mode 100644 index 000000000..1543cd3c4 --- /dev/null +++ b/examples/inbound-webhook-handler/.dockerignore @@ -0,0 +1,9 @@ +# directories +**/bin/ +**/obj/ +**/out/ + +# files +Dockerfile* +**/*.trx +**/*.md diff --git a/examples/inbound-webhook-handler/.vscode/launch.json b/examples/inbound-webhook-handler/.vscode/launch.json new file mode 100644 index 000000000..47b437d9b --- /dev/null +++ b/examples/inbound-webhook-handler/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/inbound/bin/Debug/netcoreapp2.1/inbound.dll", + "args": [], + "cwd": "${workspaceFolder}/src/inbound", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/examples/inbound-webhook-handler/.vscode/tasks.json b/examples/inbound-webhook-handler/.vscode/tasks.json new file mode 100644 index 000000000..25a4d86b5 --- /dev/null +++ b/examples/inbound-webhook-handler/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/inbound/inbound.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/examples/inbound-webhook-handler/Dockerfile b/examples/inbound-webhook-handler/Dockerfile new file mode 100644 index 000000000..5d5f9b080 --- /dev/null +++ b/examples/inbound-webhook-handler/Dockerfile @@ -0,0 +1,21 @@ +FROM microsoft/dotnet:2.1-sdk AS build +WORKDIR /App + +# copy csproj and restore as distinct layers +COPY *.sln . +COPY Src/Inbound/*.csproj ./Src/Inbound/ +COPY Tests/Inbound.Tests/*.csproj ./Tests/Inbound.Tests/ +RUN dotnet restore + +# copy everything else and build app +COPY Src/Inbound/. ./Src/Inbound/ +WORKDIR /App/Src/Inbound +RUN dotnet publish -c Release -o Out + +FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime +WORKDIR /App +COPY --from=build /App/Src/Inbound/Out ./ + +RUN echo "ASPNETCORE_URLS=http://0.0.0.0:\$PORT\nDOTNET_RUNNING_IN_CONTAINER=true" > /App/SetupHerokuEnv.sh && chmod +x /App/SetupHerokuEnv.sh + +CMD /bin/bash -c "source /App/SetupHerokuEnv.sh && dotnet Inbound.dll" diff --git a/examples/inbound-webhook-handler/README.md b/examples/inbound-webhook-handler/README.md new file mode 100644 index 000000000..f5ddb1019 --- /dev/null +++ b/examples/inbound-webhook-handler/README.md @@ -0,0 +1,93 @@ +# Overview + +SendGrid has an Email Inbound Parse Webhook which posts data from a specified incoming email address to a URL of your choice. This library allows you to quickly and easily deployable solution that help you easily get up and running processing (parse and complete some action) your inbound parse webhooks. + +This is docker-based solution which can be deployed on cloud services like Heroku out of the box. + +# Table of Content +- [Prerequisite](#prerequisite) +- [Deploy locally](#deploy_locally) +- [Deploy Heroku](#deploy_heroku) +- [Testing the Source Code](#testing_the_source_code) + + +## Prerequisite +Clone the repository +``` +git clone https://github.com/sendgrid/sendgrid-csharp.git +``` +Move into the clonned repository +``` +cd sendgrid-csharp/examples/inbound-webhook-handler +``` +Restore the Packages +``` +dotnet restore +``` + + +## Deploy locally +Setup your MX records. Depending on your domain name host, you may need to wait up to 48 hours for the settings to propagate. + +Run the Inbound Parse listener in your terminal: +``` +git clone https://github.com/sendgrid/sendgrid-csharp.git + +cd sendgrid-csharp/examples/inbound-webhook-handler + +dotnet restore + +dotnet run --project .\Src\Inbound\Inbound.csproj +``` +Above will start server listening on a random port like below + +In another terminal, use ngrok to allow external access to your machine: +``` +ngrok http PORT_NUMBER +``` +Update your SendGrid Incoming Parse settings: Settings Page | Docs + + - For the HOSTNAME field, use the domain that you changed the MX records (e.g. inbound.yourdomain.com) + - For the URL field, use the URL generated by ngrok + /inbound, e.g http://XXXXXXX.ngrok.io/inbound + +Next, send an email to [anything]@inbound.yourdomain.com, then look at the terminal where you started the Inbound Parse listener. + + +## Deploy to Heroku + +[Create](https://signup.heroku.com/) Heruko account if not already present + +Install the Heroku CLI + +Download and install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line). + +If you haven't already, log in to your Heroku account and follow the prompts to create a new SSH public key. +``` +$ heroku login +``` + +Now you can sign into Container Registry. +``` +$ heroku container:login +``` + +Create app in heroku +``` +$ heroku apps:create UNIQUE_APP_NAME +``` + +Push your Docker-based app +Build the Dockerfile in the current directory and push the Docker image. +``` +$ heroku container:push web --app UNIQUE_APP_NAME +``` + +Deploy the changes +Release the newly pushed images to deploy your app. +``` +$ heroku container:release web --app UNIQUE_APP_NAME +``` + + +## Testing the Source Code +You can get all the test cases inside the `Tests` folder. diff --git a/examples/inbound-webhook-handler/SendGridInbound.sln b/examples/inbound-webhook-handler/SendGridInbound.sln new file mode 100644 index 000000000..55234eeeb --- /dev/null +++ b/examples/inbound-webhook-handler/SendGridInbound.sln @@ -0,0 +1,59 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{5E71A0CA-F2E2-4762-B020-29F1D8682F75}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inbound", "Src\Inbound\Inbound.csproj", "{9449C214-54EF-40A9-AAB3-4FE212BECA23}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1446F41E-1766-4B3E-B7AC-C8766A3E1751}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inbound.Tests", "Tests\Inbound.Tests\Inbound.Tests.csproj", "{0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Debug|x64.ActiveCfg = Debug|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Debug|x64.Build.0 = Debug|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Debug|x86.ActiveCfg = Debug|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Debug|x86.Build.0 = Debug|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Release|Any CPU.Build.0 = Release|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Release|x64.ActiveCfg = Release|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Release|x64.Build.0 = Release|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Release|x86.ActiveCfg = Release|Any CPU + {9449C214-54EF-40A9-AAB3-4FE212BECA23}.Release|x86.Build.0 = Release|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Debug|x64.ActiveCfg = Debug|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Debug|x64.Build.0 = Debug|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Debug|x86.ActiveCfg = Debug|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Debug|x86.Build.0 = Debug|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Release|Any CPU.Build.0 = Release|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Release|x64.ActiveCfg = Release|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Release|x64.Build.0 = Release|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Release|x86.ActiveCfg = Release|Any CPU + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9449C214-54EF-40A9-AAB3-4FE212BECA23} = {5E71A0CA-F2E2-4762-B020-29F1D8682F75} + {0AF26ED1-3F2D-40F5-9A01-FE5955A8F927} = {1446F41E-1766-4B3E-B7AC-C8766A3E1751} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D6E6C0FF-04E9-4D98-B958-612DDD7FBB0A} + EndGlobalSection +EndGlobal diff --git a/examples/inbound-webhook-handler/Src/Inbound/Controllers/InboundController.cs b/examples/inbound-webhook-handler/Src/Inbound/Controllers/InboundController.cs new file mode 100644 index 000000000..a8fa52b83 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Controllers/InboundController.cs @@ -0,0 +1,40 @@ +using Inbound.Parsers; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace Inbound.Controllers +{ + [Route("/")] + [ApiController] + public class InboundController : Controller + { + [HttpGet] + public IActionResult Index() + { + return View(); + } + + // Process POST from Inbound Parse and print received data. + [HttpPost] + [Route("inbound")] + public IActionResult InboundParse() + { + InboundWebhookParser _inboundParser = new InboundWebhookParser(Request.Body); + + var inboundEmail = _inboundParser.Parse(); + + return Ok(); + } + + private void Log(IDictionary keyValues) + { + if(keyValues == null) + { + return; + } + Console.WriteLine(JsonConvert.SerializeObject(keyValues)); + } + } +} \ No newline at end of file diff --git a/examples/inbound-webhook-handler/Src/Inbound/Inbound.csproj b/examples/inbound-webhook-handler/Src/Inbound/Inbound.csproj new file mode 100644 index 000000000..95d6c7bfb --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Inbound.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + diff --git a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmail.cs b/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmail.cs new file mode 100644 index 000000000..b3aa5dd03 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmail.cs @@ -0,0 +1,145 @@ +using System.Collections.Generic; +using System.Text; + +namespace Inbound.Models +{ + /// + /// The parsed information about an email sent by SendGrid via a webhook. + /// + public class InboundEmail + { + /// + /// Gets or sets the headers of the email. + /// + /// + /// The headers. + /// + public KeyValuePair[] Headers { get; set; } + + /// + /// Gets or sets a string containing the verification results of any DKIM and domain keys signatures in the message. + /// + /// + /// The dkim. + /// + public string Dkim { get; set; } + + /// + /// Gets or sets the email recipient field, as taken from the message headers. + /// + /// + /// To. + /// + public InboundEmailAddress[] To { get; set; } + + /// + /// Gets or sets the carbon copy recipient field, as taken from the message headers. + /// + /// + /// To. + /// + public InboundEmailAddress[] Cc { get; set; } + + /// + /// Gets or sets the HTML body of email. If not set, email did not have a HTML body. + /// + /// + /// The HTML. + /// + public string Html { get; set; } + + /// + /// Gets or sets the TEXT body of email .If not set, email did not have a TEXT body. + /// + /// + /// The TEXT. + /// + public string Text { get; set; } + + /// + /// Gets or sets from the email sender, as taken from the message headers. + /// + /// + /// From. + /// + public InboundEmailAddress From { get; set; } + + /// + /// Gets or sets the sender's ip address. + /// + /// + /// The sender ip. + /// + public string SenderIp { get; set; } + + /// + /// Gets or sets the Spam Assassin's spam report. + /// + /// + /// The spam report. + /// + public string SpamReport { get; set; } + + /// + /// Gets or sets the SMTP envelope. + /// This will have two variables: + /// - to, which is a single-element array containing the address that we received the email to, + /// - from, which is the return path for the message. + /// + /// + /// The envelope. + /// + public InboundEmailEnvelope Envelope { get; set; } + + /// + /// Gets or sets the email subject. + /// + /// + /// The subject. + /// + public string Subject { get; set; } + + /// + /// Gets or sets the Spam Assassin’s rating for whether or not this is spam. + /// + /// + /// The spam score. + /// + public string SpamScore { get; set; } + + /// + /// Gets or sets the attachment. + /// + /// + /// The attachment. + /// + public InboundEmailAttachment[] Attachments { get; set; } + + /// + /// Gets or sets the character sets of the fields extracted from the message. + /// + /// + /// The charsets. + /// + public KeyValuePair[] Charsets { get; set; } + + /// + /// Gets or sets the results of the Sender Policy Framework verification of the message sender + /// and receiving IP address. + /// + /// + /// The SPF. + /// + public string Spf { get; set; } + + /// + /// Get or sets the raw email received + /// + /// + /// RAW EMAIL + /// + public string RawEmail { get; set; } + } + + +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAddress.cs b/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAddress.cs new file mode 100644 index 000000000..43e0ea724 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAddress.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; + +namespace Inbound.Models +{ + /// + /// The address for Email recipient, including the name and email address. + /// + public class InboundEmailAddress + { + /// + /// Gets or sets the email. + /// + /// + /// The email. + /// + [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] + public string Email { get; set; } + + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The email. + /// The name. + public InboundEmailAddress(string email, string name) + { + Email = email; + Name = name; + } + } + + +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAttachment.cs b/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAttachment.cs new file mode 100644 index 000000000..b548cc44c --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAttachment.cs @@ -0,0 +1,65 @@ +using Newtonsoft.Json; +using System.IO; + +namespace Inbound.Models +{ + /// + /// Strongly typed representation of the information sudmited by SendGrid in a 'inbound parse' webhook. + /// + public class InboundEmailAttachment + { + /// + /// Gets or sets the identifier. + /// + /// + /// The identifier. + /// + public string Id { get; set; } + + /// + /// Gets or sets the content-type. Defaults to text/plain if unspecified. + /// + /// + /// The content-type. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string ContentType { get; set; } + + /// + /// Gets or sets the data. + /// + /// + /// The data. + /// + public Stream Data { get; set; } + + /// + /// Gets or sets the file name. + /// + /// + /// The name of the file. + /// + [JsonProperty("filename", NullValueHandling = NullValueHandling.Ignore)] + public string FileName { get; set; } + + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + /// + /// Gets or sets the content identifier. + /// + /// + /// The content identifier. + /// + [JsonProperty("content-id", NullValueHandling = NullValueHandling.Ignore)] + public string ContentId { get; set; } + } + + +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailEnvelope.cs b/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailEnvelope.cs new file mode 100644 index 000000000..f201cb0c7 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailEnvelope.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace Inbound.Models +{ + /// + /// The SMTP envelope. + /// + public class InboundEmailEnvelope + { + /// + /// Gets or sets to, which is a single-element array containing the address that we received the email to. + /// + /// + /// To. + /// + [JsonProperty("to", NullValueHandling = NullValueHandling.Ignore)] + public string[] To { get; set; } + + /// + /// Gets or sets from, which is the return path for the message. + /// + /// + /// From. + /// + [JsonProperty("from", NullValueHandling = NullValueHandling.Ignore)] + public string From { get; set; } + } + + +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Parsers/InboundWebhookParser.cs b/examples/inbound-webhook-handler/Src/Inbound/Parsers/InboundWebhookParser.cs new file mode 100644 index 000000000..29f79037f --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Parsers/InboundWebhookParser.cs @@ -0,0 +1,142 @@ +using HttpMultipartParser; +using Inbound.Models; +using Inbound.Util; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Inbound.Parsers +{ + public class InboundWebhookParser + { + private readonly Stream _payload; + + public InboundWebhookParser(Stream stream) + { + _payload = new MemoryStream(); + stream.CopyTo(_payload); + } + + public InboundEmail Parse() + { + // It's important to rewind the stream + _payload.Position = 0; + + // Parse the multipart content received from SendGrid + var parser = new MultipartFormDataParser(_payload, Encoding.UTF8); + + // Convert the 'headers' from a string into array of KeyValuePair + var rawHeaders = parser + .GetParameterValue("headers", string.Empty) + .Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); + + var headers = rawHeaders + .Select(header => + { + var splitHeader = header.Split(new[] { ": " }, StringSplitOptions.RemoveEmptyEntries); + var key = splitHeader[0]; + var value = splitHeader.Length > 1 ? splitHeader[1] : null; + return new KeyValuePair(key, value); + }).ToArray(); + + // Raw email + var rawEmail = parser.GetParameterValue("email", string.Empty); + + // Combine the 'attachment-info' and Files into an array of Attachments + var attachmentInfoAsJObject = JObject.Parse(parser.GetParameterValue("attachment-info", "{}")); + var attachments = attachmentInfoAsJObject + .Properties() + .Select(prop => + { + var attachment = prop.Value.ToObject(); + attachment.Id = prop.Name; + + var file = parser.Files.FirstOrDefault(f => f.Name == prop.Name); + if (file != null) + { + attachment.Data = file.Data; + if (string.IsNullOrEmpty(attachment.ContentType)) attachment.ContentType = file.ContentType; + if (string.IsNullOrEmpty(attachment.FileName)) attachment.FileName = file.FileName; + } + + return attachment; + }).ToArray(); + + // Convert the 'envelope' from a JSON string into a strongly typed object + var envelope = JsonConvert.DeserializeObject(parser.GetParameterValue("envelope", "{}")); + + // Convert the 'charset' from a string into array of KeyValuePair + var charsetsAsJObject = JObject.Parse(parser.GetParameterValue("charsets", "{}")); + var charsets = charsetsAsJObject + .Properties() + .Select(prop => + { + var key = prop.Name; + var value = Encoding.GetEncoding(prop.Value.ToString()); + return new KeyValuePair(key, value); + }).ToArray(); + + // Create a dictionary of parsers, one parser for each desired encoding. + // This is necessary because MultipartFormDataParser can only handle one + // encoding and SendGrid can use different encodings for parameters such + // as "from", "to", "text" and "html". + var encodedParsers = charsets + .Where(c => c.Value != Encoding.UTF8) + .Select(c => c.Value) + .Distinct() + .Select(encoding => + { + _payload.Position = 0; // It's important to rewind the stream + return new + { + Encoding = encoding, + Parser = new MultipartFormDataParser(_payload, encoding) + }; + }) + .Union(new[] + { + new { Encoding = Encoding.UTF8, Parser = parser } + }) + .ToDictionary(ep => ep.Encoding, ep => ep.Parser); + + // Convert the 'from' from a string into an email address + var rawFrom = InboundWebhookParserHelper.GetEncodedValue("from", charsets, encodedParsers, string.Empty); + var from = InboundWebhookParserHelper.ParseEmailAddress(rawFrom); + + // Convert the 'to' from a string into an array of email addresses + var rawTo = InboundWebhookParserHelper.GetEncodedValue("to", charsets, encodedParsers, string.Empty); + var to = InboundWebhookParserHelper.ParseEmailAddresses(rawTo); + + // Convert the 'cc' from a string into an array of email addresses + var rawCc = InboundWebhookParserHelper.GetEncodedValue("cc", charsets, encodedParsers, string.Empty); + var cc = InboundWebhookParserHelper.ParseEmailAddresses(rawCc); + + // Arrange the InboundEmail + var inboundEmail = new InboundEmail + { + Attachments = attachments, + Charsets = charsets, + Dkim = InboundWebhookParserHelper.GetEncodedValue("dkim", charsets, encodedParsers, null), + Envelope = envelope, + From = from, + Headers = headers, + Html = InboundWebhookParserHelper.GetEncodedValue("html", charsets, encodedParsers, null), + SenderIp = InboundWebhookParserHelper.GetEncodedValue("sender_ip", charsets, encodedParsers, null), + SpamReport = InboundWebhookParserHelper.GetEncodedValue("spam_report", charsets, encodedParsers, null), + SpamScore = InboundWebhookParserHelper.GetEncodedValue("spam_score", charsets, encodedParsers, null), + Spf = InboundWebhookParserHelper.GetEncodedValue("SPF", charsets, encodedParsers, null), + Subject = InboundWebhookParserHelper.GetEncodedValue("subject", charsets, encodedParsers, null), + Text = InboundWebhookParserHelper.GetEncodedValue("text", charsets, encodedParsers, null), + To = to, + Cc = cc, + RawEmail = rawEmail + }; + + return inboundEmail; + } + } +} \ No newline at end of file diff --git a/examples/inbound-webhook-handler/Src/Inbound/Program.cs b/examples/inbound-webhook-handler/Src/Inbound/Program.cs new file mode 100644 index 000000000..c2828bab0 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Inbound +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Startup.cs b/examples/inbound-webhook-handler/Src/Inbound/Startup.cs new file mode 100644 index 000000000..4b9879581 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Startup.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Inbound +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseMvc(); + } + } +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Util/Extensions.cs b/examples/inbound-webhook-handler/Src/Inbound/Util/Extensions.cs new file mode 100644 index 000000000..25ff7508f --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Util/Extensions.cs @@ -0,0 +1,19 @@ +using HttpMultipartParser; + +namespace Inbound.Util +{ + public static class Extensions + { + /// + /// Returns the value of a parameter or the default value if it doesn't exist. + /// + /// The parser. + /// The name of the parameter. + /// The default value. + /// The value of the parameter. + public static string GetParameterValue(this MultipartFormDataParser parser, string name, string defaultValue) + { + return parser.HasParameter(name) ? parser.GetParameterValue(name) : defaultValue; + } + } +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Util/InboundWebhookParserHelper.cs b/examples/inbound-webhook-handler/Src/Inbound/Util/InboundWebhookParserHelper.cs new file mode 100644 index 000000000..e8f134542 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Util/InboundWebhookParserHelper.cs @@ -0,0 +1,73 @@ +using HttpMultipartParser; +using Inbound.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Inbound.Util +{ + public static class InboundWebhookParserHelper + { + public static InboundEmailAddress[] ParseEmailAddresses(string rawEmailAddresses) + { + // Split on commas that have an even number of double-quotes following them + const string SPLIT_EMAIL_ADDRESSES = ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"; + + /* + When we stop supporting .NET 4.5.2 we will be able to use the following: + if (string.IsNullOrEmpty(rawEmailAddresses)) return Array.Empty(); + */ + if (string.IsNullOrEmpty(rawEmailAddresses)) return Enumerable.Empty().ToArray(); + + var rawEmails = Regex.Split(rawEmailAddresses, SPLIT_EMAIL_ADDRESSES); + var addresses = rawEmails + .Select(rawEmail => ParseEmailAddress(rawEmail)) + .Where(address => address != null) + .ToArray(); + return addresses; + } + + public static InboundEmailAddress ParseEmailAddress(string rawEmailAddress) + { + if (string.IsNullOrEmpty(rawEmailAddress)) + { + return null; + } + + var pieces = rawEmailAddress.Split(new[] { '<', '>' }, StringSplitOptions.RemoveEmptyEntries); + + if (pieces.Length == 0) + { + return null; + } + + var email = pieces.Length == 2 ? pieces[1].Trim() : pieces[0].Trim(); + var name = pieces.Length == 2 ? pieces[0].Replace("\"", string.Empty).Trim() : string.Empty; + return new InboundEmailAddress(email, name); + } + + public static string GetEncodedValue(string parameterName, IEnumerable> charsets, + IDictionary encodedParsers, string defaultValue = null) + { + var parser = GetEncodedParser(parameterName, charsets, encodedParsers); + var value = parser.GetParameterValue(parameterName, defaultValue); + return value; + } + + private static MultipartFormDataParser GetEncodedParser(string parameterName, IEnumerable> charsets, + IDictionary encodedParsers) + { + var encoding = GetEncoding(parameterName, charsets); + var parser = encodedParsers[encoding]; + return parser; + } + + private static Encoding GetEncoding(string parameterName, IEnumerable> charsets) + { + var encoding = charsets.Where(c => c.Key == parameterName); + return encoding.Any() ? encoding.First().Value : Encoding.UTF8; + } + } +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Views/Inbound/Index.cshtml b/examples/inbound-webhook-handler/Src/Inbound/Views/Inbound/Index.cshtml new file mode 100644 index 000000000..cae53debc --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/Views/Inbound/Index.cshtml @@ -0,0 +1,10 @@ + + + SendGrid Incoming Parse + + +

You have successfully launched the server!

+ + Check out the documentation on how to use this software to utilize the SendGrid Inbound Parse webhook. + + diff --git a/examples/inbound-webhook-handler/Src/Inbound/appsettings.Development.json b/examples/inbound-webhook-handler/Src/Inbound/appsettings.Development.json new file mode 100644 index 000000000..e203e9407 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/appsettings.json b/examples/inbound-webhook-handler/Src/Inbound/appsettings.json new file mode 100644 index 000000000..def9159a7 --- /dev/null +++ b/examples/inbound-webhook-handler/Src/Inbound/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/Inbound.Tests.csproj b/examples/inbound-webhook-handler/Tests/Inbound.Tests/Inbound.Tests.csproj new file mode 100644 index 000000000..6a8c3fe97 --- /dev/null +++ b/examples/inbound-webhook-handler/Tests/Inbound.Tests/Inbound.Tests.csproj @@ -0,0 +1,44 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore\2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.dll + + + + + + Always + + + Always + + + Always + + + + diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs b/examples/inbound-webhook-handler/Tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs new file mode 100644 index 000000000..bd3edcf2f --- /dev/null +++ b/examples/inbound-webhook-handler/Tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Shouldly; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Inbound.Tests.IntegrationTests +{ + public class InboundEndpointsTests : IClassFixture> + { + private readonly WebApplicationFactory applicationFactory; + + public InboundEndpointsTests(WebApplicationFactory factory) + => applicationFactory = factory; + + [Fact] + public async Task Get_IndexPageReturnsSuccessAndCorrectContentType() + { + const string URL = "/"; + + var client = applicationFactory.CreateClient(); + var response = await client.GetAsync(URL); + response.EnsureSuccessStatusCode(); + response.Content.Headers.ContentType.MediaType.ShouldBe("text/html"); + } + + [Fact] + public async Task Get_InboundEndpointReturnsNotFound() + { + const string URL = "/inbound"; + var client = applicationFactory.CreateClient(); + var response = await client.GetAsync(URL); + response.StatusCode.ShouldBe(HttpStatusCode.NotFound); + } + + [Fact] + public async Task Post_InboundEndpointWithDefaultPayload() + { + const string URL = "/inbound"; + var data = File.ReadAllTextAsync("sample_data/default_data.txt").Result; + + var content = new StringContent(data); + content.Headers.Clear(); + content.Headers.Add("Content-Type", "multipart/form-data; boundary=xYzZY"); + + var client = applicationFactory.CreateClient(); + var response = await client.PostAsync(URL, content); + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Post_InboundEndpointWithRawPayloadWithAttachments() + { + const string URL = "/inbound"; + var data = File.ReadAllTextAsync("sample_data/raw_data_with_attachments.txt").Result; + + var content = new StringContent(data); + content.Headers.Clear(); + content.Headers.Add("Content-Type", "multipart/form-data; boundary=xYzZY"); + + var client = applicationFactory.CreateClient(); + var response = await client.PostAsync(URL, content); + response.EnsureSuccessStatusCode(); + } + } +} diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs b/examples/inbound-webhook-handler/Tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs new file mode 100644 index 000000000..3d2a8696e --- /dev/null +++ b/examples/inbound-webhook-handler/Tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs @@ -0,0 +1,121 @@ +using Inbound.Models; +using Inbound.Parsers; +using Shouldly; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Xunit; + +namespace Inbound.Tests.Parsers +{ + public class InboundWebhookParserTests + { + [Fact] + public async void DefaultPayloadWithoutAttachments() + { + Stream stream = new MemoryStream(); + await File.OpenRead("sample_data/default_data.txt").CopyToAsync(stream); + stream.Position = 0; + + var parser = new InboundWebhookParser(stream); + + InboundEmail inboundEmail = parser.Parse(); + + inboundEmail.ShouldNotBeNull(); + + inboundEmail.Headers.Except(new[] { + new KeyValuePair("MIME-Version","1.0"), + new KeyValuePair("Received","by 0.0.0.0 with HTTP; Wed, 10 Aug 2016 18:10:13 -0700 (PDT)"), + new KeyValuePair("From","Example User "), + new KeyValuePair("Date","Wed, 10 Aug 2016 18:10:13 -0700"), + new KeyValuePair("Subject","Inbound Parse Test Data"), + new KeyValuePair("To","inbound@inbound.example.com"), + new KeyValuePair("Content-Type","multipart/alternative; boundary=001a113df448cad2d00539c16e89") + }).Count().ShouldBe(0); + + inboundEmail.Dkim.ShouldBe("{@sendgrid.com : pass}"); + + inboundEmail.To[0].Email.ShouldBe("inbound@inbound.example.com"); + inboundEmail.To[0].Name.ShouldBe(string.Empty); + + inboundEmail.Html.Trim().ShouldBe("Hello SendGrid!"); + + inboundEmail.Text.Trim().ShouldBe("Hello SendGrid!"); + + inboundEmail.From.Email.ShouldBe("test@example.com"); + inboundEmail.From.Name.ShouldBe("Example User"); + + inboundEmail.SenderIp.ShouldBe("0.0.0.0"); + + inboundEmail.SpamReport.ShouldBeNull(); + + inboundEmail.Envelope.From.ShouldBe("test@example.com"); + inboundEmail.Envelope.To.Length.ShouldBe(1); + inboundEmail.Envelope.To.ShouldContain("inbound@inbound.example.com"); + + inboundEmail.Attachments.Length.ShouldBe(0); + + inboundEmail.Subject.ShouldBe("Testing non-raw"); + + inboundEmail.SpamScore.ShouldBeNull(); + + inboundEmail.Charsets.Except(new[] { + new KeyValuePair("to", Encoding.UTF8), + new KeyValuePair("html", Encoding.UTF8), + new KeyValuePair("subject", Encoding.UTF8), + new KeyValuePair("from", Encoding.UTF8), + new KeyValuePair("text", Encoding.UTF8) + }).Count().ShouldBe(0); + + inboundEmail.Spf.ShouldBe("pass"); + } + + [Fact] + public async void RawPayloadWithAttachments() + { + Stream stream = new MemoryStream(); + await File.OpenRead("sample_data/raw_data_with_attachments.txt").CopyToAsync(stream); + stream.Position = 0; + + var parser = new InboundWebhookParser(stream); + + InboundEmail inboundEmail = parser.Parse(); + + inboundEmail.ShouldNotBeNull(); + + inboundEmail.Dkim.ShouldBe("{@sendgrid.com : pass}"); + + var rawEmailTestData = await File.ReadAllTextAsync("sample_data/raw_email_with_attachments.txt"); + inboundEmail.RawEmail.Trim().ShouldBe(rawEmailTestData); + + inboundEmail.To[0].Email.ShouldBe("inbound@inbound.example.com"); + inboundEmail.To[0].Name.ShouldBe(string.Empty); + + inboundEmail.Cc.Length.ShouldBe(0); + + inboundEmail.From.Email.ShouldBe("test@example.com"); + inboundEmail.From.Name.ShouldBe("Example User"); + + inboundEmail.SenderIp.ShouldBe("0.0.0.0"); + + inboundEmail.SpamReport.ShouldBeNull(); + + inboundEmail.Envelope.From.ShouldBe("test@example.com"); + inboundEmail.Envelope.To.Length.ShouldBe(1); + inboundEmail.Envelope.To.ShouldContain("inbound@inbound.example.com"); + + inboundEmail.Subject.ShouldBe("Raw Payload"); + + inboundEmail.SpamScore.ShouldBeNull(); + + inboundEmail.Charsets.Except(new[] { + new KeyValuePair("to", Encoding.UTF8), + new KeyValuePair("subject", Encoding.UTF8), + new KeyValuePair("from", Encoding.UTF8) + }).Count().ShouldBe(0); + + inboundEmail.Spf.ShouldBe("pass"); + } + } +} diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/default_data.txt b/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/default_data.txt new file mode 100644 index 000000000..cdf52d68b --- /dev/null +++ b/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/default_data.txt @@ -0,0 +1,58 @@ +--xYzZY +Content-Disposition: form-data; name="headers" + +MIME-Version: 1.0 +Received: by 0.0.0.0 with HTTP; Wed, 10 Aug 2016 18:10:13 -0700 (PDT) +From: Example User +Date: Wed, 10 Aug 2016 18:10:13 -0700 +Subject: Inbound Parse Test Data +To: inbound@inbound.example.com +Content-Type: multipart/alternative; boundary=001a113df448cad2d00539c16e89 + +--xYzZY +Content-Disposition: form-data; name="dkim" + +{@sendgrid.com : pass} +--xYzZY +Content-Disposition: form-data; name="to" + +inbound@inbound.example.com +--xYzZY +Content-Disposition: form-data; name="html" + +Hello SendGrid! + +--xYzZY +Content-Disposition: form-data; name="from" + +Example User +--xYzZY +Content-Disposition: form-data; name="text" + +Hello SendGrid! + +--xYzZY +Content-Disposition: form-data; name="sender_ip" + +0.0.0.0 +--xYzZY +Content-Disposition: form-data; name="envelope" + +{"to":["inbound@inbound.example.com"],"from":"test@example.com"} +--xYzZY +Content-Disposition: form-data; name="attachments" + +0 +--xYzZY +Content-Disposition: form-data; name="subject" + +Testing non-raw +--xYzZY +Content-Disposition: form-data; name="charsets" + +{"to":"UTF-8","html":"UTF-8","subject":"UTF-8","from":"UTF-8","text":"UTF-8"} +--xYzZY +Content-Disposition: form-data; name="SPF" + +pass +--xYzZY-- \ No newline at end of file diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_data_with_attachments.txt b/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_data_with_attachments.txt new file mode 100644 index 000000000..ab142e253 --- /dev/null +++ b/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_data_with_attachments.txt @@ -0,0 +1,298 @@ +--xYzZY +Content-Disposition: form-data; name="dkim" + +{@sendgrid.com : pass} +--xYzZY +Content-Disposition: form-data; name="email" + +MIME-Version: 1.0 +Received: by 0.0.0.0 with HTTP; Mon, 15 Aug 2016 13:47:21 -0700 (PDT) +From: Example User +Date: Mon, 15 Aug 2016 13:47:21 -0700 +Subject: Inbound Parse Test Raw Data with Attachment +To: inbound@inbound.inbound.com +Content-Type: multipart/mixed; boundary=001a1140ffb6f4fc63053a2257e2 + +--001a1140ffb6f4fc63053a2257e2 +Content-Type: multipart/alternative; boundary=001a1140ffb6f4fc5f053a2257e0 + +--001a1140ffb6f4fc5f053a2257e0 +Content-Type: text/plain; charset=UTF-8 + +Hello SendGrid! + +--001a1140ffb6f4fc5f053a2257e0 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +Hello SendGrid! + +--001a1140ffb6f4fc5f053a2257e0-- + +--001a1140ffb6f4fc63053a2257e2 +Content-Type: image/jpeg; name="SendGrid.jpg" +Content-Disposition: attachment; filename="SendGrid.jpg" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_irwihell0 + +/9j/4AAQSkZJRgABAQABLAEsAAD/4QDKRXhpZgAATU0AKgAAAAgABwESAAMA +AAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAEx +AAIAAAARAAAAcgEyAAIAAAAUAAAAhIdpAAQAAAABAAAAmAAAAAAAAAEsAAAA +AQAAASwAAAABUGl4ZWxtYXRvciAzLjQuNAAAMjAxNjowODoxMSAxNjowODo1 +NwAAA6ABAAMAAAABAAEAAKACAAQAAAABAAACEqADAAQAAAABAAACFQAAAAD/ +4Qn2aHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVn +aW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4 +bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAg +Q29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53 +My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3Jp +cHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2Jl +LmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9l +bGVtZW50cy8xLjEvIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxNi0wOC0xMVQxNjow +ODo1NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBJbWFnZVJlYWR5Ij4gPGRj +OnN1YmplY3Q+IDxyZGY6QmFnLz4gPC9kYzpzdWJqZWN0PiA8L3JkZjpEZXNj +cmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICA8P3hwYWNrZXQgZW5kPSJ3Ij8+AP/tADhQaG90b3Nob3Ag +My4wADhCSU0EBAAAAAAAADhCSU0EJQAAAAAAENQdjNmPALIE6YAJmOz4Qn7/ +wAARCAIVAhIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQF +BgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJx +FDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdI +SUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKj +pKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx +8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QA +tREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHB +CSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldY +WVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq +srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6 +/9sAQwAcHBwcHBwwHBwwRDAwMERcRERERFx0XFxcXFx0jHR0dHR0dIyMjIyM +jIyMqKioqKioxMTExMTc3Nzc3Nzc3Nzc/9sAQwEiJCQ4NDhgNDRg5pyAnObm +5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm +5ubm/90ABAAi/9oADAMBAAIRAxEAPwDpKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooAKKKKACiiigDEnkkEzgMep71F5sn94/nTp/8AXP8A +U1FXWloeZJu7H+bJ/eP50ebJ/eP50yinYm7H+bJ/eP50ebJ/eP50yiiwXY/z +ZP7x/OjzZP7x/OmUUWC7H+bJ/eP50ebJ/eP50yiiwXY/zZP7x/OjzZP7x/Om +UUWC7H+bJ/eP50ebJ/eP50yiiwXY/wA2T+8fzo82T+8fzplFFgux/myf3j+d +Hmyf3j+dMoosF2P82T+8fzo82T+8fzplFFgux/myf3j+dHmyf3j+dMoosF2I +0sufvt+dJ5sv99vzNMbrSVVkbpuxJ5sv99vzNHmy/wB9vzNR0U7Id2SebL/f +b8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/32/M1HRRZBdknmy/32/M0ebL/fb8 +zUdFFkF2SebL/fb8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/32/M1HRRZBdknm +y/32/M0ebL/fb8zUdFFkF2SebL/fb8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/ +32/M1HRRZBdknmy/32/M0ebL/fb8zUdFFkF2SebL/fb8zR5sv99vzNR0UWQX +Z//Q6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAo +oooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKAC +iiigAooooAKKKKACiiigAooooAKKKKACiiigD//R6SiiigAooooAKKKKACii +igAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKu +xbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +CJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigD//S6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA +KKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKAC +iiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiig +AooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//T6SiiigAo +oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1N +RVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigD//U6SiiigAooooAKKKKACiiigAooooAKKKK +ACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUy +QooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3 +WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +D//V6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAo +oooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKAC +iiigAooooAKKKKACiiigAooooAKKKKACiiigD//W6SiiigAooooAKKKKACii +igAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKu +xbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +CJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigD//X6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA +KKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKAC +iiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiig +AooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//Q6SiiigAo +oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1N +RVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigD//R6SiiigAooooAKKKKACiiigAooooAKKKK +ACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUy +QooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3 +WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +D//S2i7560m9/WmnqaStbHPdj97+tG9/WmUUWC7H739aN7+tMoosF2P3v60b +39aZRRYLsfvf1o3v60yiiwXY/e/rRvf1plFFgux+9/Wje/rTKKLBdj97+tG9 +/WmUUWC7H739aN7+tMoosF2P3v60b39aZRRYLsfvf1o3v60yiiwXZlzEmVvr +UeTT5f8AWt9ajrdbHI9xcmjJpKKYhcmjJpKKAFyaMmkooAXJoyaSigBcmjJp +KKAFyaMmkooAXJoyaSigBcmjJpKKAFyaMmkooAXJoyaSigBwAPWlwKB0paBX +YmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAX +YmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXZ//9PXPU0l +KeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK +AMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFF +FABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAoooo +AKKKKACiiigAooooAKKKKACiiigD/9TXPU0lKeppK1OYKKKKACiiigAooooA +KKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyv +cKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQ +dKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi +iigD/9XXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii +igAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABR +RRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKK +KACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9bXPU0lKeppK1OYKKKK +ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqS +X/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU +AFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA +ooooAKKKKACiiigD/9fXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQA +UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9DXPU0l +KeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK +AMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFF +FABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAoooo +AKKKKACiiigAooooAKKKKACiiigD/9HXPU0lKeppK1OYKKKKACiiigAooooA +KKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyv +cKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQ +dKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi +iigD/9LXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii +igAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABR +RRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKK +KACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9PXPU0lKeppK1OYKKKK +ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqS +X/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU +AFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA +ooooAKKKKACiiigD/9TXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQA +UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9WV7mUO +Rnv6U37TN6/pUMn32+pptdyiuxxNssfaZvX9KPtM3r+lV6KOVdguyx9pm9f0 +o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXYLssfaZvX9KPtM3r+lV6KOV +dguyx9pm9f0o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXYLssfaZvX9KP +tM3r+lV6KOVdguyx9pm9f0o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXY +LssfaZvX9KPtM3r+lV6KOVdguzRSFJFEjdTyad9mi96fD/ql+lS1g27lcqK/ +2aL3o+zRe9WKKXMw5V2K/wBmi96Ps0XvViijmYcq7Ff7NF70fZoverFFHMw5 +V2K/2aL3o+zRe9WKKOZhyrsV/s0XvR9mi96sUUczDlXYr/Zovej7NF71Yoo5 +mHKuxX+zRe9H2aL3qxRRzMOVdiv9mi96Ps0XvViijmYcq7Ff7NF70fZoverF +FHMw5V2K/wBmi96Ps0XvViijmYcq7BHZwlcnP50/7FB6H86ni+4KkqHJ9zdU +422Kn2KD0P50fYoPQ/nVuilzvuP2cexU+xQeh/Oj7FB6H86t0Uc77h7OPYqf +YoPQ/nR9ig9D+dW6KOd9w9nHsVPsUHofzo+xQeh/OrdFHO+4ezj2Kn2KD0P5 +0fYoPQ/nVuijnfcPZx7FT7FB6H86PsUHofzq3RRzvuHs49ip9ig9D+dH2KD0 +P51boo533D2cexU+xQeh/Oj7FB6H86t0Uc77h7OPYqfYoPQ/nR9ig9D+dW6K +Od9w9nHsVPsUHofzo+xQeh/OrdFHO+4ezj2Kn2KD0P50fYoPQ/nVuijnfcPZ +x7H/1mSffb6mm06T77fU02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFAB +RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooo +pDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9dk +n32+pptOk++31NNrvRwsKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii +igAooooAKKKKANaH/VL9KlqKH/VL9Klrme5qgooopAFFFFABRRRQAUUUUAFF +FFABRRRQAUUUUAFFFFABRRRQAUUUUAWovuCpKji+4KkrN7nRHYKKKKQwoooo +AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//QZJ99vqab +TpPvt9TTa70cLCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK +ACiiigDWh/1S/Spaih/1S/Spa5nuaoKKKKQBRRRQAUUUUAFFFFABRRRQAUUU +UAFFFFABRRRQAUUUUAFFFFAFqL7gqSo4vuCpKze50R2CiiikMKKKKACiiigA +ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/0WSffb6mm06T77fU +02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA +1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQ +AUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooopDCiiigAooooAKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigD/9Jkn32+pptOk++31NNrvRws +KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKANaH/VL9 +KlqKH/VL9Klrme5qgooopAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAB +RRRQAUUUUAWovuCpKji+4KkrN7nRHYKKKKQwooooAKKKKACiiigAooooAKKK +KACiiigAooooAKKKKACiiigAooooA//TZJ99vqabTpPvt9TTa70cLCiiigAo +oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDWh/1S/Spaih/1 +S/Spa5nuaoKKKKQBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFF +FFAFqL7gqSo4vuCpKze50R2CiiikMKKKKACiiigAooooAKKKKACiiigAoooo +AKKKKACiiigAooooAKKKKAP/1GSffb6mm06T77fU02u9HCwooooAKKKKACii +igAooooAKKKKACiiigAooooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ +7mqCiiikAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+ +4KkqOL7gqSs3udEdgooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigA +ooooAKKKKACiiigD/9Vkn32+pptOk++31NNrvRwsKKKKACiiigAooooAKKKK +ACiiigAooooAKKKKACiiigAooooAKKKKANaH/VL9KlqKH/VL9Klrme5qgooo +pAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAWovuCpKji+ +4KkrN7nRHYKKKKQwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi +iigAooooA//WZJ99vqabTpPvt9TTa70cLCiiigAooooAKKKKACiiigAooooA +KKKKACiiigAooooAKKKKACiiigDWh/1S/Spaih/1S/Spa5nuaoKKKKQBRRRQ +AUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAFqL7gqSo4vuCpKze5 +0R2CiiikMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK +KAP/12Sffb6mm06T77fU02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFAB +RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooo +pDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9DU +OnxMSdx5pP7Oi/vNWjRV+0l3I5I9jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s +6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+8 +1H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9n +Rf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3m +rRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo +9pLuHs49iBLdUUKCeKd5K+pqWip5mPlRF5K+po8lfU1LRRdhyoi8lfU0eSvq +aloouw5UReSvqaPJX1NS0UXYcqIvJX1NHkr6mpaKLsOVEXkr6mjyV9TUtFF2 +HKiLyV9TR5K+pqWii7DlRF5K+po8lfU1LRRdhyoi8lfU0eSvqaloouw5UReS +vqaPJX1NS0UXYcqIvJX1NHkr6mpaKLsOVCKu0YFLRRSKCiiigAooooAKKKKA +CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9k= + +--001a1140ffb6f4fc63053a2257e2-- + +--xYzZY +Content-Disposition: form-data; name="to" + +inbound@inbound.example.com +--xYzZY +Content-Disposition: form-data; name="from" + +Example User +--xYzZY +Content-Disposition: form-data; name="sender_ip" + +0.0.0.0 +--xYzZY +Content-Disposition: form-data; name="envelope" + +{"to":["inbound@inbound.example.com"],"from":"test@example.com"} +--xYzZY +Content-Disposition: form-data; name="subject" + +Raw Payload +--xYzZY +Content-Disposition: form-data; name="charsets" + +{"to":"UTF-8","subject":"UTF-8","from":"UTF-8"} +--xYzZY +Content-Disposition: form-data; name="SPF" + +pass +--xYzZY-- \ No newline at end of file diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_email_with_attachments.txt b/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_email_with_attachments.txt new file mode 100644 index 000000000..fa36394b6 --- /dev/null +++ b/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_email_with_attachments.txt @@ -0,0 +1,261 @@ +MIME-Version: 1.0 +Received: by 0.0.0.0 with HTTP; Mon, 15 Aug 2016 13:47:21 -0700 (PDT) +From: Example User +Date: Mon, 15 Aug 2016 13:47:21 -0700 +Subject: Inbound Parse Test Raw Data with Attachment +To: inbound@inbound.inbound.com +Content-Type: multipart/mixed; boundary=001a1140ffb6f4fc63053a2257e2 + +--001a1140ffb6f4fc63053a2257e2 +Content-Type: multipart/alternative; boundary=001a1140ffb6f4fc5f053a2257e0 + +--001a1140ffb6f4fc5f053a2257e0 +Content-Type: text/plain; charset=UTF-8 + +Hello SendGrid! + +--001a1140ffb6f4fc5f053a2257e0 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +Hello SendGrid! + +--001a1140ffb6f4fc5f053a2257e0-- + +--001a1140ffb6f4fc63053a2257e2 +Content-Type: image/jpeg; name="SendGrid.jpg" +Content-Disposition: attachment; filename="SendGrid.jpg" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_irwihell0 + +/9j/4AAQSkZJRgABAQABLAEsAAD/4QDKRXhpZgAATU0AKgAAAAgABwESAAMA +AAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAEx +AAIAAAARAAAAcgEyAAIAAAAUAAAAhIdpAAQAAAABAAAAmAAAAAAAAAEsAAAA +AQAAASwAAAABUGl4ZWxtYXRvciAzLjQuNAAAMjAxNjowODoxMSAxNjowODo1 +NwAAA6ABAAMAAAABAAEAAKACAAQAAAABAAACEqADAAQAAAABAAACFQAAAAD/ +4Qn2aHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVn +aW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4 +bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAg +Q29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53 +My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3Jp +cHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2Jl +LmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9l +bGVtZW50cy8xLjEvIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxNi0wOC0xMVQxNjow +ODo1NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBJbWFnZVJlYWR5Ij4gPGRj +OnN1YmplY3Q+IDxyZGY6QmFnLz4gPC9kYzpzdWJqZWN0PiA8L3JkZjpEZXNj +cmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICA8P3hwYWNrZXQgZW5kPSJ3Ij8+AP/tADhQaG90b3Nob3Ag +My4wADhCSU0EBAAAAAAAADhCSU0EJQAAAAAAENQdjNmPALIE6YAJmOz4Qn7/ +wAARCAIVAhIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQF +BgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJx +FDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdI +SUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKj +pKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx +8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QA +tREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHB +CSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldY +WVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq +srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6 +/9sAQwAcHBwcHBwwHBwwRDAwMERcRERERFx0XFxcXFx0jHR0dHR0dIyMjIyM +jIyMqKioqKioxMTExMTc3Nzc3Nzc3Nzc/9sAQwEiJCQ4NDhgNDRg5pyAnObm +5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm +5ubm/90ABAAi/9oADAMBAAIRAxEAPwDpKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooAKKKKACiiigDEnkkEzgMep71F5sn94/nTp/8AXP8A +U1FXWloeZJu7H+bJ/eP50ebJ/eP50yinYm7H+bJ/eP50ebJ/eP50yiiwXY/z +ZP7x/OjzZP7x/OmUUWC7H+bJ/eP50ebJ/eP50yiiwXY/zZP7x/OjzZP7x/Om +UUWC7H+bJ/eP50ebJ/eP50yiiwXY/wA2T+8fzo82T+8fzplFFgux/myf3j+d +Hmyf3j+dMoosF2P82T+8fzo82T+8fzplFFgux/myf3j+dHmyf3j+dMoosF2I +0sufvt+dJ5sv99vzNMbrSVVkbpuxJ5sv99vzNHmy/wB9vzNR0U7Id2SebL/f +b8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/32/M1HRRZBdknmy/32/M0ebL/fb8 +zUdFFkF2SebL/fb8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/32/M1HRRZBdknm +y/32/M0ebL/fb8zUdFFkF2SebL/fb8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/ +32/M1HRRZBdknmy/32/M0ebL/fb8zUdFFkF2SebL/fb8zR5sv99vzNR0UWQX +Z//Q6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAo +oooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKAC +iiigAooooAKKKKACiiigAooooAKKKKACiiigD//R6SiiigAooooAKKKKACii +igAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKu +xbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +CJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigD//S6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA +KKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKAC +iiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiig +AooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//T6SiiigAo +oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1N +RVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigD//U6SiiigAooooAKKKKACiiigAooooAKKKK +ACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUy +QooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3 +WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +D//V6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAo +oooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKAC +iiigAooooAKKKKACiiigAooooAKKKKACiiigD//W6SiiigAooooAKKKKACii +igAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKu +xbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +CJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigD//X6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA +KKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKAC +iiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiig +AooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//Q6SiiigAo +oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1N +RVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigD//R6SiiigAooooAKKKKACiiigAooooAKKKK +ACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUy +QooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3 +WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +D//S2i7560m9/WmnqaStbHPdj97+tG9/WmUUWC7H739aN7+tMoosF2P3v60b +39aZRRYLsfvf1o3v60yiiwXY/e/rRvf1plFFgux+9/Wje/rTKKLBdj97+tG9 +/WmUUWC7H739aN7+tMoosF2P3v60b39aZRRYLsfvf1o3v60yiiwXZlzEmVvr +UeTT5f8AWt9ajrdbHI9xcmjJpKKYhcmjJpKKAFyaMmkooAXJoyaSigBcmjJp +KKAFyaMmkooAXJoyaSigBcmjJpKKAFyaMmkooAXJoyaSigBwAPWlwKB0paBX +YmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAX +YmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXZ//9PXPU0l +KeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK +AMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFF +FABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAoooo +AKKKKACiiigAooooAKKKKACiiigD/9TXPU0lKeppK1OYKKKKACiiigAooooA +KKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyv +cKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQ +dKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi +iigD/9XXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii +igAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABR +RRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKK +KACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9bXPU0lKeppK1OYKKKK +ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqS +X/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU +AFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA +ooooAKKKKACiiigD/9fXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQA +UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9DXPU0l +KeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK +AMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFF +FABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAoooo +AKKKKACiiigAooooAKKKKACiiigD/9HXPU0lKeppK1OYKKKKACiiigAooooA +KKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyv +cKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQ +dKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi +iigD/9LXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii +igAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABR +RRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKK +KACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9PXPU0lKeppK1OYKKKK +ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqS +X/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU +AFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA +ooooAKKKKACiiigD/9TXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQA +UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9WV7mUO +Rnv6U37TN6/pUMn32+pptdyiuxxNssfaZvX9KPtM3r+lV6KOVdguyx9pm9f0 +o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXYLssfaZvX9KPtM3r+lV6KOV +dguyx9pm9f0o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXYLssfaZvX9KP +tM3r+lV6KOVdguyx9pm9f0o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXY +LssfaZvX9KPtM3r+lV6KOVdguzRSFJFEjdTyad9mi96fD/ql+lS1g27lcqK/ +2aL3o+zRe9WKKXMw5V2K/wBmi96Ps0XvViijmYcq7Ff7NF70fZoverFFHMw5 +V2K/2aL3o+zRe9WKKOZhyrsV/s0XvR9mi96sUUczDlXYr/Zovej7NF71Yoo5 +mHKuxX+zRe9H2aL3qxRRzMOVdiv9mi96Ps0XvViijmYcq7Ff7NF70fZoverF +FHMw5V2K/wBmi96Ps0XvViijmYcq7BHZwlcnP50/7FB6H86ni+4KkqHJ9zdU +422Kn2KD0P50fYoPQ/nVuilzvuP2cexU+xQeh/Oj7FB6H86t0Uc77h7OPYqf +YoPQ/nR9ig9D+dW6KOd9w9nHsVPsUHofzo+xQeh/OrdFHO+4ezj2Kn2KD0P5 +0fYoPQ/nVuijnfcPZx7FT7FB6H86PsUHofzq3RRzvuHs49ip9ig9D+dH2KD0 +P51boo533D2cexU+xQeh/Oj7FB6H86t0Uc77h7OPYqfYoPQ/nR9ig9D+dW6K +Od9w9nHsVPsUHofzo+xQeh/OrdFHO+4ezj2Kn2KD0P50fYoPQ/nVuijnfcPZ +x7H/1mSffb6mm06T77fU02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFAB +RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooo +pDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9dk +n32+pptOk++31NNrvRwsKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii +igAooooAKKKKANaH/VL9KlqKH/VL9Klrme5qgooopAFFFFABRRRQAUUUUAFF +FFABRRRQAUUUUAFFFFABRRRQAUUUUAWovuCpKji+4KkrN7nRHYKKKKQwoooo +AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//QZJ99vqab +TpPvt9TTa70cLCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK +ACiiigDWh/1S/Spaih/1S/Spa5nuaoKKKKQBRRRQAUUUUAFFFFABRRRQAUUU +UAFFFFABRRRQAUUUUAFFFFAFqL7gqSo4vuCpKze50R2CiiikMKKKKACiiigA +ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/0WSffb6mm06T77fU +02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA +1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQ +AUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooopDCiiigAooooAKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigD/9Jkn32+pptOk++31NNrvRws +KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKANaH/VL9 +KlqKH/VL9Klrme5qgooopAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAB +RRRQAUUUUAWovuCpKji+4KkrN7nRHYKKKKQwooooAKKKKACiiigAooooAKKK +KACiiigAooooAKKKKACiiigAooooA//TZJ99vqabTpPvt9TTa70cLCiiigAo +oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDWh/1S/Spaih/1 +S/Spa5nuaoKKKKQBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFF +FFAFqL7gqSo4vuCpKze50R2CiiikMKKKKACiiigAooooAKKKKACiiigAoooo +AKKKKACiiigAooooAKKKKAP/1GSffb6mm06T77fU02u9HCwooooAKKKKACii +igAooooAKKKKACiiigAooooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ +7mqCiiikAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+ +4KkqOL7gqSs3udEdgooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigA +ooooAKKKKACiiigD/9Vkn32+pptOk++31NNrvRwsKKKKACiiigAooooAKKKK +ACiiigAooooAKKKKACiiigAooooAKKKKANaH/VL9KlqKH/VL9Klrme5qgooo +pAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAWovuCpKji+ +4KkrN7nRHYKKKKQwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi +iigAooooA//WZJ99vqabTpPvt9TTa70cLCiiigAooooAKKKKACiiigAooooA +KKKKACiiigAooooAKKKKACiiigDWh/1S/Spaih/1S/Spa5nuaoKKKKQBRRRQ +AUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAFqL7gqSo4vuCpKze5 +0R2CiiikMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK +KAP/12Sffb6mm06T77fU02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAo +oooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFAB +RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooo +pDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9DU +OnxMSdx5pP7Oi/vNWjRV+0l3I5I9jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s +6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+8 +1H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9n +Rf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3m +rRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo +9pLuHs49iBLdUUKCeKd5K+pqWip5mPlRF5K+po8lfU1LRRdhyoi8lfU0eSvq +aloouw5UReSvqaPJX1NS0UXYcqIvJX1NHkr6mpaKLsOVEXkr6mjyV9TUtFF2 +HKiLyV9TR5K+pqWii7DlRF5K+po8lfU1LRRdhyoi8lfU0eSvqaloouw5UReS +vqaPJX1NS0UXYcqIvJX1NHkr6mpaKLsOVCKu0YFLRRSKCiiigAooooAKKKKA +CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9k= + +--001a1140ffb6f4fc63053a2257e2-- \ No newline at end of file