ABP and maildev

@jack 7/15/2025 5:13:55 PM

Introduction

In this article, we’ll show you how to easily view emails sent by your ABP application with minimal effort.

This can be helpful during the development phase for obvious reasons.

We’ll use maildev for this. However, what we’re demonstrating should work with similar tools as well.

Preparation

First, we start a maildev Docker container:

docker run -d `
  --name your-maildev `
  -e MAILDEV_SMTP_PORT=1025 `
  -e MAILDEV_WEB_PORT=8787 `
  -e MAILDEV_WEB_USER=root `
  -e MAILDEV_WEB_PASS=root `
  --network your-network `
  -p 8787:8787 `
  -p 1025:1025 `
  maildev/maildev:2.1.1

Since maildev doesn’t require authentication, we’ll need to write a small helper class as the default EmailSender does require username and password being set.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.Emailing;
using Volo.Abp.Emailing.Smtp;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Validation;

namespace ConiDerp.Emailing;

public class PasswordlessDevMailSender(ICurrentTenant currentTenant,
        ISmtpEmailSenderConfiguration smtpConfiguration,
        IBackgroundJobManager backgroundJobManager) : IEmailSender, ISmtpEmailSender
{
    protected ICurrentTenant CurrentTenant { get; } = currentTenant;

    protected ISmtpEmailSenderConfiguration SmtpConfiguration { get; } = smtpConfiguration;

    protected IBackgroundJobManager BackgroundJobManager { get; } = backgroundJobManager;

    public virtual async Task SendAsync(string to, string? subject, string? body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
    {
        await SendAsync(BuildMailMessage(null, to, subject, body, isBodyHtml, additionalEmailSendingArgs));
    }

    public virtual async Task SendAsync(string from, string to, string? subject, string? body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
    {
        await SendAsync(BuildMailMessage(from, to, subject, body, isBodyHtml, additionalEmailSendingArgs));
    }

    protected virtual MailMessage BuildMailMessage(string? from, string to, string? subject, string? body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
    {
        var message = from == null
            ? new MailMessage { To = { to }, Subject = subject, Body = body, IsBodyHtml = isBodyHtml }
            : new MailMessage(from, to, subject, body) { IsBodyHtml = isBodyHtml };

        if (additionalEmailSendingArgs != null)
        {
            if (additionalEmailSendingArgs.Attachments != null)
            {
                foreach (var attachment in additionalEmailSendingArgs.Attachments.Where(x => x.File != null))
                {
                    var fileStream = new MemoryStream(attachment.File!);
                    fileStream.Seek(0, SeekOrigin.Begin);
                    message.Attachments.Add(new Attachment(fileStream, attachment.Name));
                }
            }

            if (additionalEmailSendingArgs.CC != null)
            {
                foreach (var cc in additionalEmailSendingArgs.CC)
                {
                    message.CC.Add(cc);
                }
            }
        }

        return message;
    }

    public virtual async Task SendAsync(MailMessage mail, bool normalize = true)
    {
        if (normalize)
        {
            await NormalizeMailAsync(mail);
        }

        await SendEmailAsync(mail);
    }

    public virtual async Task QueueAsync(string to, string subject, string body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
    {
        ValidateEmailAddress(to);

        if (!BackgroundJobManager.IsAvailable())
        {
            await SendAsync(to, subject, body, isBodyHtml, additionalEmailSendingArgs);
            return;
        }

        await BackgroundJobManager.EnqueueAsync(
            new BackgroundEmailSendingJobArgs
            {
                TenantId = CurrentTenant.Id,
                To = to,
                Subject = subject,
                Body = body,
                IsBodyHtml = isBodyHtml,
                AdditionalEmailSendingArgs = additionalEmailSendingArgs
            }
        );
    }

    public virtual async Task QueueAsync(string from, string to, string subject, string body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
    {
        ValidateEmailAddress(to);

        if (!BackgroundJobManager.IsAvailable())
        {
            await SendAsync(from, to, subject, body, isBodyHtml, additionalEmailSendingArgs);
            return;
        }

        await BackgroundJobManager.EnqueueAsync(
            new BackgroundEmailSendingJobArgs
            {
                TenantId = CurrentTenant.Id,
                From = from,
                To = to,
                Subject = subject,
                Body = body,
                IsBodyHtml = isBodyHtml,
                AdditionalEmailSendingArgs = additionalEmailSendingArgs
            }
        );
    }

    /// <summary>
    /// Should implement this method to send email in derived classes.
    /// </summary>
    /// <param name="mail">Mail to be sent</param>
    protected async Task SendEmailAsync(MailMessage mail)
    {
        using (var smtpClient = await BuildClientAsync())
        {
            await smtpClient.SendMailAsync(mail);
        }
    }

    public async Task<SmtpClient> BuildClientAsync()
    {
        var host = await SmtpConfiguration.GetHostAsync();
        var port = await SmtpConfiguration.GetPortAsync();

        var smtpClient = new SmtpClient(host, port);

        try
        {
            if (await SmtpConfiguration.GetEnableSslAsync())
            {
                smtpClient.EnableSsl = true;
            }

            if (await SmtpConfiguration.GetUseDefaultCredentialsAsync())
            {
                smtpClient.UseDefaultCredentials = true;
            }
            else
            {
                smtpClient.UseDefaultCredentials = false;
                // this is the part from the original code that we do not need
                //var userName = await SmtpConfiguration.GetUserNameAsync();
                //if (!userName.IsNullOrEmpty())
                //{
                //    var password = await SmtpConfiguration.GetPasswordAsync();
                //    var domain = await SmtpConfiguration.GetDomainAsync();
                //    smtpClient.Credentials = !domain.IsNullOrEmpty()
                //        ? new NetworkCredential(userName, password, domain)
                //        : new NetworkCredential(userName, password);
                //}
            }

            return smtpClient;
        }
        catch
        {
            smtpClient.Dispose();
            throw;
        }
    }

    /// <summary>
    /// Normalizes given email.
    /// Fills <see cref="MailMessage.From"/> if it's not filled before.
    /// Sets encodings to UTF8 if they are not set before.
    /// </summary>
    /// <param name="mail">Mail to be normalized</param>
    protected virtual async Task NormalizeMailAsync(MailMessage mail)
    {
        if (mail.From == null || mail.From.Address.IsNullOrEmpty())
        {
            mail.From = new MailAddress(
                await SmtpConfiguration.GetDefaultFromAddressAsync(),
                await SmtpConfiguration.GetDefaultFromDisplayNameAsync(),
                Encoding.UTF8
                );
        }

        mail.HeadersEncoding ??= Encoding.UTF8;

        mail.SubjectEncoding ??= Encoding.UTF8;

        mail.BodyEncoding ??= Encoding.UTF8;
    }

    private static void ValidateEmailAddress(string emailAddress)
    {
        if (ValidationHelper.IsValidEmailAddress(emailAddress))
        {
            return;
        }

        throw new ArgumentException($"Email address '{emailAddress}' is not valid!");
    }
}

And now we replace the following line in our DomainModule:

#if DEBUG
context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullMailSender>());
#endif

with:

#if DEBUG
context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, PasswordlessDevMailSender>());
#endif

Finally, we configure the email settings either via appsettings.Development.json or through the UI:

{
  "Settings": {
    "Abp.Mailing.DefaultFromAddress": "some-mail@some-provider.de",
    "Abp.Mailing.DefaultFromDisplayName": "HelloItsMe",
    "Abp.Mailing.Smtp.Host": "127.0.0.1",
    "Abp.Mailing.Smtp.Port": "1025",
    "Abp.Mailing.Smtp.EnableSsl": "false",
    "Abp.Mailing.Smtp.UseDefaultCredentials": "false",
    "Abp.Mailing.Smtp.UserName": "",
    "Abp.Mailing.Smtp.Password": ""
  },
  "DetailedErrors": true
}

Result

That's it :)

go to http://localhost:8787 and login using the credentials set for the web ui.

Last Modification : 7/15/2025 5:28:44 PM


abp

Cookies

This site uses cookies.
This website uses cookies. Some cookies are technically necessary, others are used to analyze user behavior in order to optimize the offer. You can find an explanation in our Privacy Policy.