> ## Documentation Index
> Fetch the complete documentation index at: https://docs.diyanet.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Email Servisi

> Brevo SMTP üzerinden Scriban şablonlu e-posta gönderimi; ResilientEmailGateway ile MailKit + Polly retry/circuit-breaker koruması.

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

```csharp theme={null}
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](https://github.com/scriban/scriban) ile render edilir. Hangi dosyanın kullanılacağı `EmailTemplateType` enumeration'ı belirler:

```csharp theme={null}
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):

```csharp theme={null}
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);
}
```

<Note>
  Şablon HTML dosyalarının çıktıya kopyalanması için `.csproj`'da `CopyToOutputDirectory` ayarı gerekir; aksi halde `AppContext.BaseDirectory/Templates` altında bulunamaz.
</Note>

## 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:

```csharp theme={null}
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):

```csharp theme={null}
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:

```csharp theme={null}
_policyWrapper = Policy.WrapAsync(
    CreateRetryPolicy(_options.CurrentValue.RetryCount),  // WaitAndRetry, 2^attempt sn
    CreateCircuitBreakerPolicy());                        // 6 hata → 1 dk açık kalır
```

<Warning>
  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.
</Warning>

## Config — `Services:Email`

```json theme={null}
{
  "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>" ]
    }
  }
}
```

<ParamField path="SmtpHost" type="string">Brevo SMTP relay (`smtp-relay.brevo.com`).</ParamField>
<ParamField path="SmtpPort" type="int" default="25">SMTP portu — Brevo için 587.</ParamField>
<ParamField path="EnableSsl" type="bool">`true` → SslOnConnect, `false` → StartTls.</ParamField>
<ParamField path="Username" type="string">Hem SMTP login hem de gönderen adresi (From) olarak kullanılır.</ParamField>
<ParamField path="Password" type="string">Brevo SMTP Key.</ParamField>
<ParamField path="RetryCount" type="int" default="2">Polly retry sayısı (exponential backoff).</ParamField>
<ParamField path="Mails" type="string[]">Production **dışı** ortamlarda tüm mailler bu adreslere yönlendirilir.</ParamField>

## DI kaydı

```csharp theme={null}
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:

```csharp theme={null}
_emailService.SendBackground(
    to: user.Email.Value,
    subject: "Giriş doğrulama kodunuz",
    templateType: EmailTemplateType.OtpVerification,
    model: new { Code = otpCode.Value, Name = user.FullName?.Value });
```

<CardGroup cols={2}>
  <Card title="SMS Servisi" icon="comment-sms" href="/services/sms">
    OTP'nin SMS kanalı — NetGSM REST v2.
  </Card>

  <Card title="Domain Events" icon="bolt" href="/domain/domain-events">
    E-posta gönderimini tetikleyen `UserOtpGenerated` event akışı.
  </Card>
</CardGroup>
