Skip to main content
E-posta servisi src/DiyanetCleanArchitecture.Infrastructure.Services.Email projesindedir. İki katmandan oluşur:
  • EmailService : IEmailService — şablon render (Scriban), ortam bazlı adres yönlendirme, üst-seviye API.
  • ResilientEmailGateway : IResilientEmailGateway — MailKit ile SMTP gönderimi, Polly retry + circuit-breaker.

Arayüz

public interface IEmailService
{
    // Arka planda (fire-and-forget) gönderim — UI akışını bloklamaz
    void SendBackground(string to, string subject, EmailTemplateType templateType, object model);

    // Beklemeli (awaitable) şablonlu gönderim
    Task<bool> SendAsync(string to, string subject, EmailTemplateType templateType, object model, CancellationToken ct = default);

    // Şablonsuz, hazır HTML gönderimi
    Task<bool> SendRawAsync(string to, string subject, string htmlContent, CancellationToken ct = default);
}
SendBackground, Task.Run içinde SendAsync’i çağırır; hata olursa loglar ama fırlatmaz — login akışı gibi kullanıcıyı bekletmemesi gereken yerlerde kullanılır.

Şablonlar — Scriban

Şablonlar Templates/*.html altında tutulur ve Scriban ile render edilir. Hangi dosyanın kullanılacağı EmailTemplateType enumeration’ı belirler:
public class EmailTemplateType : Enumeration
{
    public static EmailTemplateType Welcome             = new(1, "welcome-template");
    public static EmailTemplateType OtpVerification     = new(2, "otp-verification");
    public static EmailTemplateType MagicLink           = new(3, "magic-link");
    public static EmailTemplateType AccountAlert        = new(4, "account-alert");
    public static EmailTemplateType DailyReport         = new(5, "daily-report");
    public static EmailTemplateType IntegrationError    = new(6, "integration-error");
    public static EmailTemplateType SubscriptionExpiring= new(7, "subscription-expiring");
    public static EmailTemplateType PaymentSuccess      = new(8, "payment-success");
}
Name aynı zamanda dosya adıdır. Render sırasında dosya AppContext.BaseDirectory/Templates/{Name}.html yolundan okunur (Docker/K8s’te path güvenliği için AppContext.BaseDirectory kullanılır):
private async Task<string> RenderScribanTemplate(string templateFileName, object model)
{
    string templatePath = Path.Combine(AppContext.BaseDirectory, "Templates", $"{templateFileName}.html");

    if (!File.Exists(templatePath))
        throw new EmailServiceException($"Şablon dosyası bulunamadı: {templatePath}");

    string rawContent = await File.ReadAllTextAsync(templatePath);

    var template = Template.Parse(rawContent);
    if (template.HasErrors)
        throw new EmailServiceException($"Scriban şablon hatası: {string.Join(" | ", template.Messages)}");

    return await template.RenderAsync(model);
}
Şablon HTML dosyalarının çıktıya kopyalanması için .csproj’da CopyToOutputDirectory ayarı gerekir; aksi halde AppContext.BaseDirectory/Templates altında bulunamaz.

Ortam bazlı adres yönlendirme

Production dışı ortamlarda gerçek alıcıya gönderim yapılmaz — gerçek müşterilere yanlışlıkla mail gitmesini engellemek için tüm gönderim Services:Email:Mails[] listesindeki test adreslerine yönlendirilir:
private string GetToAddress(string to)
{
    if (_environment.IsProduction()) return to;

    if (_options.Value.Mails == null || !_options.Value.Mails.Any())
        throw new EmailServiceException("Dev/Staging ortamı için test e-postası tanımlanmamış.");

    return string.Join(',', _options.Value.Mails.Select(m => m.Trim()));
}
Cc ve Bcc adresleri ise hem çağrıdan gelen hem de config’teki değerlerin birleşiminden (distinct) oluşur.

ResilientEmailGateway — MailKit + Polly

Gerçek gönderim internal ResilientEmailGateway içinde olur. MailKit SmtpClient ile bağlanır; EnableSsl true ise SslOnConnect, değilse StartTls kullanılır (Brevo için StartTls önerilir):
await client.ConnectAsync(
    _options.CurrentValue.SmtpHost,
    _options.CurrentValue.SmtpPort,
    _options.CurrentValue.EnableSsl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTls,
    cancellation);

await client.AuthenticateAsync(
    _options.CurrentValue.Username,
    _options.CurrentValue.Password,   // Brevo SMTP Key
    cancellation);

await client.SendAsync(email, cancellation);
Tüm gönderim bir Polly PolicyWrap içinde çalışır:
_policyWrapper = Policy.WrapAsync(
    CreateRetryPolicy(_options.CurrentValue.RetryCount),  // WaitAndRetry, 2^attempt sn
    CreateCircuitBreakerPolicy());                        // 6 hata → 1 dk açık kalır
Circuit-breaker eşiği koddadır: 6 ardışık hata sonrası devre 1 dakika açık kalır (exceptionsAllowedBeforeBreaking: 6, durationOfBreak: 1 dk). Retry sayısı ise config’ten (RetryCount, default 2) gelir.

Config — Services:Email

{
  "Services": {
    "Email": {
      "SmtpHost": "smtp-relay.brevo.com",
      "SmtpPort": 587,
      "EnableSsl": false,
      "DisplayName": "diyanetcleanarchitecture",
      "Username": "no-reply@diyanetcleanarchitecture",
      "Password": "mypassword",
      "TimeoutSeconds": 30,
      "RetryCount": 2,
      "Cc": [],
      "Bcc": [],
      "Mails": [ "Mustafa Cihan DELİPINAR <cihandelipinar@gmail.com>" ]
    }
  }
}
SmtpHost
string
Brevo SMTP relay (smtp-relay.brevo.com).
SmtpPort
int
default:"25"
SMTP portu — Brevo için 587.
EnableSsl
bool
true → SslOnConnect, false → StartTls.
Username
string
Hem SMTP login hem de gönderen adresi (From) olarak kullanılır.
Password
string
Brevo SMTP Key.
RetryCount
int
default:"2"
Polly retry sayısı (exponential backoff).
Mails
string[]
Production dışı ortamlarda tüm mailler bu adreslere yönlendirilir.

DI kaydı

public static IServiceCollection AddEmailService(this IServiceCollection services, IConfiguration configuration)
{
    services.Configure<EmailServiceOptions>(o => configuration.GetSection("Services:Email").Bind(o));

    services.AddScoped<IResilientEmailGateway, ResilientEmailGateway>();
    services.AddScoped<IEmailService, EmailService>();

    // SMTP health check — [External] tag: SMTP down olunca /health/ready BOZULMAZ
    services.AddHealthChecks()
        .AddSmtpHealthCheck(opts => { opts.Host = smtpHost; opts.Port = smtpPort; },
            name: "smtp",
            failureStatus: HealthStatus.Degraded,
            tags: new[] { HealthCheckTags.External, HealthCheckTags.Email },
            timeout: TimeSpan.FromSeconds(5));

    return services;
}

Kullanım

OTP e-postası gönderimi, UserOtpGeneratedDomainEvent dinleyen bir domain event handler içinde yapılır (örn. SendOtpEmailDomainEventHandler). Login gibi kritik akışlarda fire-and-forget tercih edilir:
_emailService.SendBackground(
    to: user.Email.Value,
    subject: "Giriş doğrulama kodunuz",
    templateType: EmailTemplateType.OtpVerification,
    model: new { Code = otpCode.Value, Name = user.FullName?.Value });

SMS Servisi

OTP’nin SMS kanalı — NetGSM REST v2.

Domain Events

E-posta gönderimini tetikleyen UserOtpGenerated event akışı.