Эта статья предназначена для новичков и рассматривает микросервис отправки писем через smtp Yandex с использованием .Net Web Api и MailKit. В ней будет рассмотрено, как настроить и использовать данный микросервис для отправки писем с помощью почтового сервиса через существующий почтовый ящик Яндекса.

Ссылки для понимания и работы

GitHub Документация Яндекс

Дерево проекта:

```csharp
public class MailSettings {
    public string? DisplayName { get; set; }
    public string? From { get; set; }
    public string? UserName { get; set; }
    public string? Password { get; set; }
    public string? Host { get; set; }
    public int Port { get; set; }
    public bool UseSSL { get; set; }
    public bool UseStartTls { get; set; }
    public bool UseOAuth { get; set; }
}

В данном коде определен класс MailSettings, который представляет настройки для отправки писем через smtp Yandex с использованием .Net Web Api и MailKit. В классе определены различные свойства, такие как DisplayName, From, UserName, Password, Host, Port, UseSSL, UseStartTls и UseOAuth, которые позволяют задать соответствующие параметры для подключения и аутентификации к почтовому серверу Яндекса.

namespace mailService.Models;

public class MailData {
    // Receiver
    public List<string> To { get; }
    public List<string> Bcc { get; }

    public List<string> Cc { get; }

    // Sender
    public string? From { get; }

    public string? DisplayName { get; }

    public string? ReplyTo { get; }

    public string? ReplyToName { get; }

    // Content
    public string Subject { get; }

    public string? Body { get; }

    public MailData(
        List<string> to,
        string subject,
        string? body = null,
        string? from = null,
        string? displayName = null,
        string? replyTo = null,
        string? replyToName = null,
        List<string>? bcc = null,
        List<string>? cc = null
    ) {
        // Receiver
        To = to;
        Bcc = bcc ?? new List<string>();
        Cc = cc ?? new List<string>();

        // Sender
        From = from;
        DisplayName = displayName;
        ReplyTo = replyTo;
        ReplyToName = replyToName;

        // Content
        Subject = subject;
        Body = body;
    }
}

Класс MailData представляет собой структуру данных для отправки электронных писем через smtp Yandex с использованием .Net Web Api и MailKit. Он имеет различные свойства, такие как To, Bcc, Cc, From, DisplayName, ReplyTo, ReplyToName, Subject и Body, которые позволяют указать необходимую информацию для письма, такую как получатели, отправитель, тема и содержание.

```csharp
public interface IMailService
{
    Task<bool> SendAsync(MailData mailData, CancellationToken ct);
}

```

Интерфейс IMailService определяет контракт для работы с отправкой писем через smtp Yandex с использованием .Net Web Api и MailKit. Метод SendAsync принимает объект MailData и токен отмены (CancellationToken) в качестве параметров и возвращает задачу (Task) с логическим значением (bool), указывающим, было ли письмо успешно отправлено. Этот интерфейс позволяет абстрагироваться от конкретной реализации отправки писем и обеспечивает гибкость в дальнейшей работе с отправкой писем.

using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Options;
using MimeKit;
using mailService.Configuration;
using mailService.Models;

namespace mailService.Services {
    public class MailService: IMailService {
        private readonly MailSettings _settings;

        public MailService(IOptions<MailSettings> settings) {
            _settings = settings.Value;
        }

        public async Task<bool> SendAsync(MailData mailData) {
            try {
                // Initialize a new instance of the MimeKit.MimeMessage class
                var mail = new MimeMessage();

                #region Sender / Receiver
                // Sender
                mail.From.Add(new MailboxAddress(_settings.DisplayName, mailData.From ?? _settings.From));
                mail.Sender = new MailboxAddress(mailData.DisplayName ?? _settings.DisplayName, mailData.From ?? _settings.From);

                // Receiver
                foreach (string mailAddress in mailData.To)
                    mail.To.Add(MailboxAddress.Parse(mailAddress));

                // Set Reply to if specified in mail data
                if(!string.IsNullOrEmpty(mailData.ReplyTo))
                    mail.ReplyTo.Add(new MailboxAddress(mailData.ReplyToName, mailData.ReplyTo));

                // BCC
                // Check if a BCC was supplied in the request
                if (mailData.Bcc != null)
                {
                    // Get only addresses where value is not null or with whitespace. x = value of address
                    foreach (string mailAddress in mailData.Bcc.Where(x => !string.IsNullOrWhiteSpace(x)))
                        mail.Bcc.Add(MailboxAddress.Parse(mailAddress.Trim()));
                }

                // CC
                // Check if a CC address was supplied in the request
                if (mailData.Cc != null)
                {
                    foreach (string mailAddress in mailData.Cc.Where(x => !string.IsNullOrWhiteSpace(x)))
                        mail.Cc.Add(MailboxAddress.Parse(mailAddress.Trim()));
                }
                #endregion

                #region Content

                var body = new BodyBuilder();
                mail.Subject = mailData.Subject;
                body.HtmlBody = mailData.Body;
                mail.Body = body.ToMessageBody();

                #endregion

                #region Send Mail
                
                using (var client = new SmtpClient()) {
                    try {
                        await client.ConnectAsync(_settings.Host, _settings.Port, SecureSocketOptions.SslOnConnect);
                        client.AuthenticationMechanisms.Remove("XOAUTH2");
                        client.Authenticate(_settings.UserName, _settings.Password);
                        await client.SendAsync(mail);
                    }
                    catch (Exception e) {
                        Console.WriteLine(e);
                        throw;
                    }
                    finally {
                        await client.DisconnectAsync(true);
                        client.Dispose();
                    }
                }

                #endregion

                return true;
            }
            catch (Exception) {
                return false;
            }
        }
    }
}

Этот код представляет собой службу для отправки электронной почты с использованием библиотеки MailKit. Он включает в себя класс MailService , который реализует интерфейс IMailService .

В конструкторе класса MailService используется зависимость IOptions<MailSettings> settings , чтобы получить настройки электронной почты из конфигурации.

Метод SendAsync асинхронно отправляет электронное письмо на основе переданных данных. Он создает экземпляр класса MimeMessage и настраивает отправителя, получателя, копию, скрытую копию, тему и тело письма. Затем он использует SmtpClient для подключения к серверу SMTP, аутентификации и отправки письма.

Если отправка прошла успешно, метод возвращает true , в противном случае - false .

using mailService.Models;
using mailService.Services;
using Microsoft.AspNetCore.Mvc;

namespace mailService.Controllers; 
[Route("api/[controller]")]
[ApiController]
public class MailController: ControllerBase {
    private readonly IMailService _mail;

    public MailController(IMailService mail)
    {
        _mail = mail;
    }

    [HttpPost("sendmail")]
    public async Task<IActionResult> SendMailAsync(MailData mailData)
    {
        bool result = await _mail.SendAsync(mailData);

        if (result)
        {
            return StatusCode(StatusCodes.Status200OK, "Mail has successfully been sent.");
        } 
        else
        {
            return StatusCode(StatusCodes.Status500InternalServerError, "An error occured. The Mail could not be sent.");
        }
    }
}

В данном коде представлен контроллер MailController, который отвечает за обработку HTTP-запросов для отправки электронной почты. Контроллер принимает зависимость IMailService в конструкторе и содержит метод SendMailAsync, отвечающий за отправку письма.

Метод SendMailAsync является действием, доступным по HTTP-маршруту /api/mail/sendmail и принимает объект MailData в качестве параметра. Внутри метода вызывается метод SendAsync зависимости IMailService для отправки письма. В зависимости от результата отправки, метод возвращает соответствующий статус HTTP и сообщение о результате операции.

Контроллер помечен атрибутами [Route("api/[controller]")] и [ApiController], которые определяют префикс маршрута для контроллера и обеспечивают автоматическую валидацию модели запроса.

Таким образом, данный код представляет собой контроллер API для отправки электронной почты, который использует сервис IMailService для выполнения операции отправки.

using mailService.Configuration;
using mailService.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.Configure<MailSettings>(
        builder
            .Configuration
            .GetSection(nameof(MailSettings))
    );
builder.Services.AddTransient<IMailService, MailService>();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Здесь настраивается конфигурация приложения ASP.NET Core. В данном коде применяются следующие настройки:

  • AddControllers() - добавляет контроллеры в контейнер зависимостей.

  • AddEndpointsApiExplorer() - добавляет поддержку Swagger/OpenAPI для документирования API.

  • AddSwaggerGen() - добавляет поддержку Swagger для генерации документации API.

  • Configure<MailSettings>() - конфигурирует настройки почты из секции MailSettings в файле конфигурации.

  • AddTransient<IMailService, MailService>() - регистрирует сервис IMailService с временным временем жизни, чтобы он мог быть использован в приложении.

Далее создается экземпляр приложения app и настраивается конвейер обработки HTTP-запросов. В зависимости от окружения (разработка или другое), применяются соответствующие настройки, такие как использование Swagger. Затем добавляются промежуточные компоненты для перенаправления HTTPS, авторизации и маршрутизации запросов к контроллерам. Наконец, приложение запускается с помощью app.Run().

Для запуска приложения, исходные данные берутся из файлов appsettings.Development.json и appsettings.json. Эти файлы содержат конфигурационные значения, такие как настройки почты, которые используются для настройки приложения. В файле appsettings.Development.json содержатся значения, применяемые во время разработки, в то время как appsettings.json содержит значения для других окружений, таких как продакшн. Эта структура конфигурации позволяет легко настраивать приложение в различных средах без необходимости изменения исходного кода. Для того что бы мои данные остались моими я приведу картинки из этих файлов, они одинаковые у меня.

Откуда взять пароль? Читайте с документации Яндекс

Тэстовые данные для отправки

{
  "to": [
    "shadow-greg@yandex.ru"
  ],
  "bcc": [
    "gregosetrov@yandex.ru"
  ],
  "cc": [
    "gregosetrov@yandex.ru"
  ],
  "from": "gregosetrov@yandex.ru",
  "displayName": "greg",
  "replyTo": "",
  "replyToName": "string",
  "subject": "string",
  "body": "string"
}

Пожалуйста, обратите внимание, что все поля с адресами электронной почты (to, bcc, cc, from) должны быть заполнены при отправке письма. В противном случае, сервер может вернуть ошибку 500.

Заключение:

В данной статье был представлен код для создания микросервиса отправки писем через SMTP Yandex с использованием библиотеки MailKit в .NET Web API. Были показаны основные компоненты кода, такие как класс MailService для отправки писем, контроллер MailController для обработки HTTP-запросов и настройка конфигурации приложения.

Код предоставляет пример реализации функциональности отправки писем с помощью библиотеки MailKit и демонстрирует использование интерфейсов и зависимостей для обеспечения расширяемости и тестируемости. Были рассмотрены основные шаги отправки письма, включая создание объекта MimeMessage, настройку отправителя и получателя, подключение к серверу SMTP и отправку письма.

Также были предоставлены примеры данных для тестирования отправки писем, а также информация о том, откуда можно получить пароль для авторизации при использовании почты Яндекс.

В целом, представленный код и описание позволяют разработчикам создать микросервис для отправки писем с использованием SMTP Yandex и библиотеки MailKit в .NET Web API. Это может быть полезно для различных проектов, где требуется отправка электронных писем из приложения.

Комментарии (16)


  1. vasiliy_stankevich
    01.11.2023 04:36

    Как всегда привязка к Asp .Net приложению цветет и пахнет.... А если убрать эту привязку то вполне можно обойтись 1 сущностью для работы с e-mail, которую кстати очень удобно будет интегрировать в DI контейнер ????


  1. ShadowGreg Автор
    01.11.2023 04:36

    Да это тоже вариант, но задача - сделать микросервис который работает с Rabbit и на него ещё накручу пару функций своих, поэтому .Net.


    1. lair
      01.11.2023 04:36

      В смысле - работающий с раббит? Читающий оттуда? Или пишущий? Или что? Какая при этом связь с .Net (ну, кроме того, что вам, видимо, удобнее на нем писать)?


      1. ShadowGreg Автор
        01.11.2023 04:36

        Читающий. А микросервис работающий на API можно и без .Net организовать, но думаю это будет более изощрённо и не так поворотливо.


        1. lair
          01.11.2023 04:36

          Читающий.

          Зачем вам тогда asp.net для этого?

          А микросервис работающий на API можно и без .Net организовать

          Что такое "работающий на API"?

          Вы точно не путаете .net и asp.net?


  1. AirLight
    01.11.2023 04:36

    У меня на проекте примерно такой же, только с разными провайдерами работает, в том числе Mailchimp, и нет контроллеров, только потребляет сообщения из очереди.


  1. Ksey_N
    01.11.2023 04:36

    Для запуска приложения, исходные данные берутся из файлов appsettings.Development.json и appsettings.json. Эти файлы содержат конфигурационные значения, такие как настройки почты, которые используются для настройки приложения. В файле appsettings.Development.json содержатся значения, применяемые во время разработки, в то время как appsettings.json содержит значения для других окружений, таких как продакшн

    Ну вообще не совсем так.

    Конфигурацию строит специальный билдер, она собирается в следующем порядке:
    1.appsetting.json
    2.appsetting.{ТЕКУЩЕЕ ОКРУЖЕНИЕ}.json
    3.переменные окружения

    Исходя из порядка по умолчанию appsetting.{ТЕКУЩЕЕ ОКРУЖЕНИЕ}.json переопределяет или создаёт конфигурационную секцию, которая будет использоваться только в текущем окружении, a appsetting.json содержит общую конфигурацию, которая будет использоваться в любом окружении.

    Как рекомендация, данные, которые могут быть скомпрометированы (здесь это логин и пароль от смтп-клиента) лучше хранить именно в переменных окружения, чтобы усложнить их учетку к третьим лицам.

    П.С.

    Тэстовые данные для отправки


    Мои глаза истекли кровью, на этом предложении)))


  1. ShadowGreg Автор
    01.11.2023 04:36

    Спасибо за комментарий!


  1. OwDafuq
    01.11.2023 04:36

    1. Почему проект называется в lowerCamelCase?

    2. using (var client = new SmtpClient()), а потом client.Dispose();, а зачем Dispose?

      1. Давно уже существует конструкция using var, имхо, но для глаза приятнее

    3. Какие-то магические Bcc и Cc в конструкторе, что они означают? (если комментарии в полном коде у конструктора есть, то вопрос отпадает)

    4. Не придирка, а просто вопросы:

      1. не пробовали все файлы настроек складывать в отдельную папку? Имхо, но мне показалось это в разы удобнее, чем когда они находятся в корне.

      2. не пробовали разделение настроек, чтобы не все в appsettings.json складывать? я обычно такие вещи разбиваю на логические куски, например: SerilogSettings.json (настройки для Serilog'a), MongoSettings.json (настройки для MongoDB), etc. В вашем примере хорошо (ИМХО!!!) подошло разделение конфига: appsettings.json, mailsettings.json


    1. Vitimbo
      01.11.2023 04:36

      1. Не совсем магические, если учитывать контекст. Это отправка копий письма дополнительным адресатам, но да, лучше бы добавить комментарий, чтобы не было вопросов.


  1. ShadowGreg Автор
    01.11.2023 04:36

    спасибо за клёвый комментарий!


  1. lair
    01.11.2023 04:36

    ...вот я вижу у вас в коде UseAuthorization. А зачем? У вас есть авторизация? На чем она основана?

    Или вот зачем вам сваггер?

    Пожалуйста, обратите внимание, что все поля с адресами электронной почты (to, bcc, cc, from) должны быть заполнены при отправке письма. В противном случае, сервер может вернуть ошибку 500.

    А почему 500, если это совершенно банальная ошибка валидации входных данных?


    1. ShadowGreg Автор
      01.11.2023 04:36

      Клёво спасибо за то что заметили что можно доработать! 500 - потому севак яндекса выдавал такую ошибку в дебагере на сколько я помню.


      1. lair
        01.11.2023 04:36

        500 - потому севак яндекса выдавал такую ошибку в дебагере на сколько я помню.

        Так почему у вас невалидные данные вообще отправляются в Яндекс?


  1. lair
    01.11.2023 04:36

    ...или вот у вас есть настройка:

    различные свойства, такие как [...] UseOAuth, которые позволяют задать соответствующие параметры для подключения и аутентификации к почтовому серверу Яндекса.

    И я такой: хм, интересно, как у вас там OAuth сделан. А потом выясняется, что настройка-то нигде не используется, зато:

    client.AuthenticationMechanisms.Remove("XOAUTH2");

    А пароль при этом, небось, лежит в плейнтексте, потому что работы с секретами я у вас тоже не вижу.

    Грустно это всё.


  1. nronnie
    01.11.2023 04:36

    Метод MailService.SendAsync нарушает сразу два принципа - "Single Responsibility" и "Dependency Inversion". Он делает сразу две вещи - создает MimeMessage из MailData и отправляет его в SmtpClient, и делает это обращаясь к нему напрямую (new SmtpClient()), а не через абстракцию. На практике результатом будет то, что, например, невозможно будет оттестировать создание MimeMessage не отправляя при этом его в "настоящий" SMTP.