Skip to content

Mailer Wrapper for sending Text and HTML Mails (using Razor) for the new MailKit Client.

License

Notifications You must be signed in to change notification settings

nfMalde/AspNetCore.MailKitMailer

Repository files navigation

Nuget Downloads Paypal Donate Pull Request Check

AspNetCore.MailKitMailer (by Malte)

This Mail Client is based on MailKit to provide HTML-Emails rendered by razor view engine for .NET 10.x, with support for inline/embedded images via CID (Content-ID).

Other Versions

All other version except 2.1.x are outdated and not maintained anymore. Please upgrade to the latest version if possible.

Third Party Dependencies

Install

Using the nuget package manager:

Install-Package AspNetCore.MailKitMailer

Using the dotnet cli:

dotnet add package AspNetCore.MailKitMailer

Enable it:

// Startup.cs

 public IServiceProvider ConfigureServices(IServiceCollection services) {
     
    services.AddAspNetCoreMailKitMailer(Configuration)
                .RegisterAllMailContexesOfCallingAssembly(); // This will add all MailerContexes defined in the calling assembly (see below for more options)
 }

Configuration

You can configure the mailer via IConfiguration using your appsettings.json (I highly recommend to use the AppSecrets in production mode for storing your password/login)

{
"MailKitMailer": {
    "Host": "localhost",
    "Port": 0,
    "UseSSL": true,
    "CheckCertificateRevocation": false,
    "Username": "user",
    "Password": "pass",
    "FromAddress": {
      "Name": "Community",
      "Email": "noreply@localhost"
    }
  }
}

Configuration Entries and their meanings

Configuration Entry Name Description Default Value Type
Host The host to the smtp server null String
Port The Port to connect to the smtp server 0 Integer
UseSSL Enforces SSL usage for smtp connection false Boolean
CheckCertificateRevocation Force MailKot to dont check for certificate revocation false Boolean
Username The Username to authenticate to the smtp server null String
Password The Password to authenticate to the smtp server null String
FromAddress The From Address for all mails (default from address) null FromAddress
FromAddress.Name The name for the from address (e.g. John Doe) undefined String
FromAddress.Email The email for the from address (e.g. john.doe@localhost) undefined String

Usage

Preparing your view folder

Create an folder in your root web project folder called Mailer-Views Add the following files with its contents for the start:

_ViewStart.cshtml

@{
    Layout = "_Layout";
}

_ViewImports.cshtml

@using MailKitMailerExample
@using MailKitMailerExample.Models
@using MailKitMailerExample.Models.MailModels
@using AspNetCore.MailKitMailer.Helper

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AspNetCore.MailKitMailer // this will add the mailkit css helper to inline css files

Shared/_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Integration Tests</title>
</head>
<body>

    <div class="inttest-body">
        @RenderBody()
    </div>

</body>
</html>

Using the "partial" Tag Helper

In order to use the partial tag helper <partial name="viewname" for="Model"/> you need to adjust your settings.

1. Create a CustomViewLocationExpander class

To add the "Mailer-Views" path to the actual razor view lookup paths you need an LocatioExpander Class of type IViewLocationExpander.

 public class CustomViewLocationExpander : IViewLocationExpander
 {
     public void PopulateValues(ViewLocationExpanderContext context)
     {
         // Can use context to add custom values based on route or other data
     }

     public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
     {
         // Add custom locations
         return new[]
         {
         "/Mailer-Views/{1}/{0}.cshtml", // Add Mailer Views to the context
     }.Concat(viewLocations); // Ensure default locations are preserved
     }
 }
2. Configure the razor view engine using your ViewLocationExpander Class

Now head to your Program.cs or for old approach Startup.cs.

Add to your services:

// New approach
builder.Services.Configure<RazorViewEngineOptions>(options =>
{
    options.ViewLocationExpanders.Add(new CustomViewLocationExpander());
});

// Old approach
public void ConfigureServices(IServiceCollection services)
{
   services.Configure<RazorViewEngineOptions>(options =>
   {
       options.ViewLocationExpanders.Add(new CustomViewLocationExpander());
   });
}

Now the partial view tag helper which is built-in will look into our Mailer-Views folder.

Now your view folder is set up for using the mailer.

Creating your contex

Fist we need to create or mailing contex. Create an class called "TesTmailer".

Our Testmailer class will extend AspNetCore.MailKitMailer.Data.MailerContextAbstract.

using System;
using System.Collections.Generic;
using System.Linq; 
using AspNetCore.MailKitMailer.Data;
using AspNetCore.MailKitMailer.Domain;
using AspNetCore.MailKitMailer.Models;
using MailKitMailerExample.Models.MailModels;

namespace MailKitMailerExample.Mailer
{
    public class TestMailer : MailerContextAbstract
    {
        public IMailerContextResult WelcomeMail(string username, string email)
        {
            return this.HtmlMail(new EmailAddressModel(username, email),
                $"Welcome {username}!",

                new WelcomeModel() { Username = username, Date = DateTime.Now });
        } 

        public async Task<IMailerContextResult> WelcomeMailAsync(string username, string email)
        {

            var taskResult  =  await Task.FromResult("MyName");
            
            return this.HtmlMail(new EmailAddressModel(taskResult, email),
                $"Welcome {username}!",

                new WelcomeModel() { Username = username, Date = DateTime.Now });
        } 
    }
}

For explaining: The Method "WelcomeMaiL" will prepare an HtmlMessage (Possible is also plain text Message. Use the helper function "TextMessage" in this case)

Since our method is called "WelcomeMail" and we didnt provide an addtitional view name the mailer will try to render the view "WelcomeMail" in the ~/Mailer-Views/TestMailer/ or ~/Views/Mailer/TestMailer directory. Fallback paths for this would be ~/Views/Mailer/WelcomeMail.cshtml or ~/Mailer-Views/WelcomeMail.cshtml (For async: WelcomeMailAsync it will look for WelcomeMailAsync.cshtml)

All we need to do now is extracting our class to an Interface that extends AspNetCore.MailKitMailer.Domain.IMailerContext

public interface ITestMailer:AspNetCore.MailKitMailer.Domain.IMailerContext
{
    IMailerContextResult WelcomeMail(string username, string email);
    Task<IMailerContextResult> WelcomeMailAsync(string username, string email);
}

public class TestMailer : MailerContextAbstract, ITestMailer
{
        ...
    
}

Now we create a view located in ~/Mailer-Views/TestMailer called WelcomeMail.cshtml

Sending the Mail

Assuming we are using it inside an MvcController all we have to do is to inject the IMailClient.

using AspNetCore.MailKitMailer.Domain;
using MailKitMailerExample.Mailer;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MailKitMailerExample.Controllers
{
    [Route("test")]
    public class TestController : Controller
    {
        private readonly IMailClient client;

        public TestController(IMailClient client)
        {
            this.client = client;
        }

        [HttpGet("welcome")]
        public IActionResult Welcome()
        {
            string username = "John.Doe";
            string useremail = "john@example.com";

            this.client.Send<ITestMailer>(x => x.WelcomeMail(username, useremail));
            
            return View();
        } 
         
        [HttpGet("welcome-async")]
        public async Task<IActionResult> WelcomeAsync()
        {
            string username = "John.Doe";
            string useremail = "john@example.com";

            await this.client.SendAsync<ITestMailer>(x => x.WelcomeMailAsync(username, useremail));
            
            return View();
        } 
    }
}

As we see we are injecting the mail client and calling our contex to prepare the message. Then we are sending it, in one line.

Retrieving the rendered mail content

With Version 2.0.1 added: The GetContentAsync Method It renders the html body of your context call and returns the HTML as string.

Helpfull to display your email in a browser or for debugging your styles.

namespace MailKitMailerExample.Mailer.Controllers
{
    public class TestController(IMailClient client) : Controller
    { 

        [HttpGet("[action]")]
        public async Task<IActionResult> DebugMail()
        {
             
             string content = await client.GetContentAsync<ITestMailer>(x => x.Test_Single_Values(payload));

            return Content(content, "text/html");
        } 
    }
}

Default Values

Our contex can provide an load of default values. Lets assume our welcome mail should go also to "admin@example.com":

namespace MailKitMailerExample.Mailer
{
    public class TestMailer : MailerContextAbstract, ITestMailer
    {
        public TestMailer() {
            this.DefaultReceipients.Add(new EmailAddressModel("admin", "admin@localhost"));
        }

        public IMailerContextResult WelcomeMail(string username, string email)
        {
            return this.HtmlMail(new EmailAddressModel(username, email),
                $"Welcome {username}!",

                new WelcomeModel() { Username = username, Date = DateTime.Now });
        } 
    }
}

In the contructor you see we are adding the admin@locahost address to DefaultReceipients. This will cause if we send an mail we will also send it to all addresses located in DefaultReceipients. Same for DefaultCCReceipients for cc and DefaultBCCReceipients for bcc.

Sending Attachments

Sending attachment is as easy as sending an default mail.

Lets create a new contex method for this in our TestMailer:

public IMailerContextResult Test_Attachment(string attachmentPath)
{
return HtmlMail(
    new EmailAddressModel("test", "test@localhost"),
    "Test-Attachment",
    null,
    null,
    x => x.Add(attachmentPath) 
    );
}

You see something different here. We have this anonymous function that adds an file path for some kind of collection. Thats our attachment collection. It will hold information of planned attachments and its content type. On send the mail client will resolve this and add it to the mail. You can also download files to add as attachments. Just provide an type of "Uri" then. Second paramter in our x.Add method is the content type. So you can override the content type if youn want to.

Extract it to our interface and test it.

Inline Images with CID (Content-ID)

CID (Content-ID) allows you to embed images directly into your HTML emails. Instead of linking to external URLs, images are included as part of the email and referenced using cid: URLs. This ensures images display correctly even when the recipient is offline or has external image loading disabled.

Step 1: Create the Mailer Method

In your mailer class, use the withAttachments parameter to add images as linked resources with a unique Content-ID:

using AspNetCore.MailKitMailer.Data;
using AspNetCore.MailKitMailer.Domain;
using AspNetCore.MailKitMailer.Models;

namespace MailKitMailerExample.Mailer
{
    public interface ITestMailer : IMailerContext
    {
        IMailerContextResult WelcomeMailWithLogo(string username, string email, string logoPath);
    }

    public class TestMailer : MailerContextAbstract, ITestMailer
    {
        /// <summary>
        /// Sends a welcome email with an embedded company logo.
        /// The logo will be embedded directly in the email, not loaded from an external URL.
        /// </summary>
        public IMailerContextResult WelcomeMailWithLogo(string username, string email, string logoPath)
        {
            return this.HtmlMail(
                new EmailAddressModel(username, email),
                $"Welcome {username}!",
                new WelcomeModel() { Username = username, Date = DateTime.Now },
                // Use withAttachments to add the image as a linked resource
                // "company-logo" is the Content-ID that will be used to reference this image in HTML
                withAttachments: a => a.AddLinkedResource(logoPath, "company-logo"));
        }
    }
}
Step 2: Create the View with CID References

Create a Razor view file in your Mailer-Views/TestMailer/ folder (e.g., WelcomeMailWithLogo.cshtml).

Reference the embedded image using src="cid:your-content-id":

@model WelcomeModel

<div style="text-align: center; padding: 20px;">
    <!-- Reference the embedded image using cid: followed by the Content-ID -->
    <img src="cid:company-logo" alt="Company Logo" style="max-width: 200px;" />
</div>

<h1>Welcome @Model.Username!</h1>
<p>Thank you for joining us on @Model.Date.ToLongDateString()!</p>

<p>Best regards,<br/>The Team</p>
Step 3: Send the Email from a Controller
[HttpGet("welcome")]
public IActionResult SendWelcomeEmail()
{
    string username = "John.Doe";
    string email = "john@example.com";
    
    // Get the path to the logo image in wwwroot/images
    string logoPath = Path.Combine(_webHost.WebRootPath, "images", "logo.png");

    // Send the email - the logo will be embedded inline
    _client.Send<ITestMailer>(x => x.WelcomeMailWithLogo(username, email, logoPath));
    
    return Ok("Email sent!");
}
Adding Multiple Inline Images

You can add multiple linked resources from different sources (file paths, byte arrays, or URLs):

public IMailerContextResult NewsletterWithImages(string email, byte[] headerImage, string footerImagePath)
{
    return this.HtmlMail(
        new EmailAddressModel("Subscriber", email),
        "Monthly Newsletter",
        new NewsletterModel(),
        withAttachments: a =>
        {
            // From byte array (e.g., dynamically generated image)
            a.AddLinkedResource(headerImage, "image/png", "header-image", "header.png");
            
            // From file path
            a.AddLinkedResource(footerImagePath, "footer-image");
            
            // From URL (image is downloaded when the email is sent)
            a.AddLinkedResource(new Uri("https://example.com/logo.png"), "external-logo");
        });
}

Then in your view:

<img src="cid:header-image" alt="Header" />
<p>Newsletter content here...</p>
<img src="cid:footer-image" alt="Footer" />
<img src="cid:external-logo" alt="Logo" />
Important Notes
  • The contentId parameter should NOT include the cid: prefix. Use "company-logo" not "cid:company-logo".
  • In your HTML/Razor view, reference it as src="cid:company-logo".
  • The withAttachments parameter is also available on PlainTextMail, though inline images via CID are only rendered in HTML email clients.

Auto-Registering Contexes

AspNetCore.MailKitMailer is able to register all mail contexes to services that match an certain criteria: The class has to be public The class exntends AspNetCore.MailKitMailer.Data.MailerContextAbstract. If you want to register it as interface your interface needs to also extend AspNetCore.MailKitMailer.Domain.IMailerContext. This all done your mail contex is automaically available via dependency injection.

Tag Helper

Ive created an tag helper based on the old approach of https://www.meziantou.net/inlining-a-stylesheet-using-a-taghelper-in-asp-net-core.htm

    <mailer-inline-style href="css/site.css"></mailer-inline-style>

This call will load the file css/site.css file as inline style. By default the base path is the Web-Root e.g. "wwwroot". You can change this by setting use-content-root to true. If this cases it uses the main content root.

Caching The tag supports caching. It will cache if an memory cache is registered the result in the memory cache. If an distributed cache is registerted it will prefer this.

To force the use of memory cache you can set force-memory-cache to true.

Url Helper

AspNetCore.MailKitMailer comes with an UrlHelper to provide AbsoluteUrls for mail bodies.

Usage:

href="@Url.MailerAbsoluteAction("Index","Home")"

This will generate an absolute url to the action Index of Controller Home. See https://github.com/nfMalde/AspNetCore.MailKitMailer/blob/main/src/AspNetCore.MailKitMailer/Helper/UrlHelper.cs for more info.

Examples

There is an fully working example .net core project located at https://github.com/nfMalde/AspNetCore.MailKitMailer/tree/main/examples/MailKitMailerExample. Feel free to download it and play arround. Also this project got a few integration tests where you can see all different type of usages.

Contribute / Donations

If you got any Ideas to improve my projects feel free to send an pull request.

If you like my work and want to support me (or want to buy me a coffee/beer) paypal donation are more than appreciated.

Paypal Donate

About

Mailer Wrapper for sending Text and HTML Mails (using Razor) for the new MailKit Client.

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published

Contributors 3

  •  
  •  
  •