This repository contains MailService.Api, a small ASP.NET Core app that exposes a single HTTP endpoint for sending e-mail. Incoming requests never contain SMTP passwords: the server looks up SMTP settings by the sender address you supply.
Flow:
- The client sends a
POSTwith JSON (Content-Type: application/json) to/api/mail/send. - The endpoint checks the
apiKeyagainstMAIL_API_KEYfrom the.envfile. - It resolves
fromEmailto an SMTP profile inSMTP_CONFIG_JSON(each allowed sender is a key in that JSON object). - It validates recipients, subject, and that at least one of
plainTextBodyorhtmlBodyis present. EmailSenderServicebuilds a MIME message and sends it with MailKit using the matched host, port, SSL, and credentials.
Typical HTTP results:
| Status | Meaning |
|---|---|
200 |
Success; JSON body includes a confirmation message. |
401 |
API key missing or wrong. |
400 |
Validation error (e.g. unknown fromEmail, empty toEmails, missing body). |
500 |
SMTP or internal failure while sending (details in server logs). |
Property names in JSON are camelCase (for example fromEmail, toEmails, plainTextBody).
The API exposes one endpoint:
POST /api/mail/send
Each request must include:
apiKey: API key that must matchMAIL_API_KEYin.envfromEmail: sender e-mail address that must exist in SMTP mappingstoEmails: list of recipient addressessubject: mail subjectplainTextBodyorhtmlBody: at least one must be present (see below if you send both)
The service validates:
- API key
- Sender (
fromEmail) exists inSMTP_CONFIG_JSON - Request payload contains required fields
If valid, it selects SMTP configuration for the sender and sends the message through that SMTP server using MailKit.
The service does not pick only one of them or concatenate them into a single blob. It uses MimeKit’s BodyBuilder with both values set, which produces a standard multipart/alternative MIME message: both a plain text part and an HTML part are included in the same outgoing e-mail.
Recipients’ mail programs then choose what to display: clients that support HTML usually show the HTML part, and plain-text-only clients (or “view as plain text”) typically show the plain text part. You should treat the two fields as the same content in two formats (for example HTML newsletter plus an accessible text fallback), not as two unrelated bodies.
If you supply only plainTextBody or only htmlBody, the message is sent with that single part only.
Place a .env file next to the project (same folder as MailService.Api.csproj):
MailService.Api/.env
Required variables:
MAIL_API_KEYSMTP_CONFIG_JSON(JSON object keyed by sender e-mail)
Example:
MAIL_API_KEY=replace_with_strong_secret_key
SMTP_CONFIG_JSON={"noreply@example.com":{"host":"smtp.example.com","port":587,"enableSsl":true,"username":"smtp-user","password":"smtp-password","fromName":"Mail Service","fromEmail":"noreply@example.com"}}cd MailService.Api
dotnet runBy default (see Properties/launchSettings.json) the app listens on http://localhost:5127 and https://localhost:7012. Use the same base URL in your client examples.
These steps assume a Windows machine with IIS acting as the reverse proxy; the ASP.NET Core Module (ANCM) starts your app in a worker process.
- Turn on Internet Information Services and the features you need (at minimum Web Management Tools and World Wide Web Services). On Windows Server this is done in Server Manager → Add Roles and Features; on Windows client editions, Turn Windows features on or off.
- Install the ASP.NET Core Hosting Bundle for the same major .NET version as the app. This repository targets
net10.0(MailService.Api.csproj), so install the bundle that includes that runtime. The hosting bundle installs the .NET runtime and the IIS AspNetCoreModuleV2 shim.
Restart IIS after installing the bundle (for example iisreset from an elevated command prompt, or restart the server).
On a machine with the SDK, from the repository root:
dotnet publish .\MailService.Api\MailService.Api.csproj -c Release -o C:\Publish\MailService.ApiUse any output folder you prefer; that folder becomes the IIS site’s physical path.
The publish output includes web.config, which tells IIS to forward requests to your app via ANCM.
The app loads .env from its content root—on IIS, that is the site’s physical path (the publish folder), not the project folder on your dev machine.
Copy a valid .env file into the publish directory next to MailService.Api.dll (same place as web.config), with MAIL_API_KEY and SMTP_CONFIG_JSON as described in Configuration (.env).
Ensure the IIS app pool identity can read that file (and the folder). Avoid granting broader permissions than needed.
- Open IIS Manager → Application Pools → Add Application Pool.
- Name: e.g.
MailService.Api. - .NET CLR version: No Managed Code (ASP.NET Core runs out-of-process; the CLR setting does not load classic .NET Framework).
- Managed pipeline mode: Integrated (default).
- Name: e.g.
- Advanced Settings (optional but common): set Identity to a dedicated account if you do not want ApplicationPoolIdentity.
- Under Sites → Add Website:
- Physical path: your publish folder (e.g.
C:\Publish\MailService.Api). - Binding: host name, port, and (for HTTPS) certificate as required. There is no fixed port; use 80/443 or another port your clients will call.
- Physical path: your publish folder (e.g.
Start the site (and ensure the app pool is started). Your API base URL is then http://your-server/api/... or https://your-server/api/... according to the binding—for example POST https://your-server/api/mail/send. If the app is installed under an application or virtual directory (for example /MailService), prefix that path: POST https://your-server/MailService/api/mail/send.
- For HTTPS, create a binding in IIS and assign a server certificate (internal CA, public CA, or development certificate).
- Open Windows Defender Firewall (or your network firewall) for the HTTP/HTTPS ports you expose.
- 502.5 / app won’t start: Usually a missing runtime or bad
web.config. Confirm the hosting bundle version matchesnet10.0and check Event Viewer → Windows Logs → Application for errors from IIS AspNetCore Module or .NET Runtime. - 401/400 from the API when the app does run: configuration or request shape; see the REST sections above.
- stdout logs: You can enable temporary stdout logging in
web.configfor the published app per Microsoft’s ASP.NET Core IIS troubleshooting (disable or remove in production once resolved).
As an alternative to .env, you can set environment variables on the app pool or system for the same keys your deployment process supports; this app is written to read secrets from .env at startup, so for IIS the straightforward approach is a .env file in the publish folder unless you change configuration sources.
{
"apiKey": "replace_with_strong_secret_key",
"fromEmail": "noreply@example.com",
"toEmails": ["recipient@example.com"],
"subject": "Test message",
"plainTextBody": "Hello from MailService."
}Use HttpClient and PostAsJsonAsync. The snippet below uses camelCase property names in an anonymous type so the JSON matches what the API expects.
using System.Net.Http.Json;
const string baseUrl = "http://localhost:5127"; // or https://localhost:7012
using var client = new HttpClient { BaseAddress = new Uri(baseUrl) };
var payload = new
{
apiKey = "replace_with_strong_secret_key",
fromEmail = "noreply@example.com",
toEmails = new[] { "recipient@example.com" },
subject = "Test message",
plainTextBody = "Hello from MailService.",
// htmlBody = "<p>Hello from MailService.</p>", // optional; use instead of or with plainTextBody
};
using var response = await client.PostAsJsonAsync("/api/mail/send", payload);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"Mail API returned {(int)response.StatusCode}: {errorBody}");
}
// Success: e.g. { "message": "Mail sent successfully." }
var json = await response.Content.ReadAsStringAsync();For a shared client library, you can replace the anonymous type with a class whose properties use [JsonPropertyName("apiKey")] (and so on) or configure JsonSerializerOptions with PropertyNamingPolicy = JsonNamingPolicy.CamelCase.
Send JSON with curl (or any HTTP client). Encode the body with json_encode and set Content-Type: application/json.
<?php
$baseUrl = 'http://localhost:5127'; // or https://localhost:7012
$url = rtrim($baseUrl, '/') . '/api/mail/send';
$payload = [
'apiKey' => 'replace_with_strong_secret_key',
'fromEmail' => 'noreply@example.com',
'toEmails' => ['recipient@example.com'],
'subject' => 'Test message',
'plainTextBody' => 'Hello from MailService.',
// 'htmlBody' => '<p>Hello from MailService.</p>',
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
]);
$responseBody = curl_exec($ch);
if ($responseBody === false) {
throw new RuntimeException('cURL error: ' . curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
throw new RuntimeException("Mail API returned HTTP $status: $responseBody");
}
// Success: e.g. {"message":"Mail sent successfully."}
echo $responseBody;If you use Guzzle, the same payload can be sent with POST $url, ['json' => $payload].