# SMTP Connection

This page explains how `SMTP.SendEmail()` picks a connection mode by port, when it authenticates, and the message format it sends.

## Preflight Checks

`SendEmail()` (`internal/service/smtp.go:28`) runs these checks in order before connecting and returns an error on the first failure:

| Check | Error message |
|-------|---------------|
| `enabled` is `true` and `host` is non-empty | `SMTP not Enabled or Host is not set` |
| `to` has at least one recipient | `no user email configured` |
| A TCP connection to `host:port` succeeds within 5 seconds | `failed to connect TCP (...)` |

## Connection Modes

```mermaid
graph TB
    Probe[TCP probe 5s] --> Plain[smtp.Dial plain connection]
    Plain -->|succeeds| Port587{port = 587?}
    Plain -->|fails and port = 465| Implicit[tls.Dial implicit TLS]
    Plain -->|fails on other ports| Fail[Return error]
    Port587 -->|yes and server supports it| StartTLS[STARTTLS upgrade]
    Port587 -->|no| Auth
    StartTLS --> Auth{username and password both set?}
    Implicit --> Auth
    Auth -->|yes| Login[PLAIN auth]
    Auth -->|no| Send
    Login --> Send[MAIL FROM / RCPT TO / DATA]
```

| Port | Actual behavior |
|------|-----------------|
| `465` | Tries plain SMTP first; an implicit-TLS server never sends a plaintext greeting, so the plain attempt fails only after the server closes the connection, and only then does `tls.Dial` open an implicit TLS connection, which can delay delivery |
| `587` | After the plain connection, upgrades to TLS when the server advertises the `STARTTLS` extension; continues in plaintext when it does not |
| Others (e.g. `25`) | Plain connection only, no TLS upgrade |

TLS always verifies the server certificate with `host` as the `ServerName`.

## Authentication

`smtp.PlainAuth` runs only when both `username` and `password` are non-empty; if either is empty, authentication is skipped, which suits internal relays that need no login. Go's standard-library PLAIN auth sends credentials only over TLS or to `localhost`, so setting credentials on an unencrypted port such as `25` fails authentication.

## Message Format

| Header | Value |
|--------|-------|
| From | `config.from` |
| To | `config.to` joined with commas |
| Cc | `config.cc`; `config.from` when empty |
| Subject | Alert or test subject |
| Content-Type | `text/html; charset=UTF-8` |

`RCPT TO` goes only to addresses in the `to` list, while Cc exists only in the header; see [Known Limitations](/known-limitations).
