Tools
More ways to use this page
SMTP submission
Use SMTP when a system cannot call an HTTP API, such as an ERP or a printer. A message sent over SMTP goes through the same checks as an API send. It appears in the same log, gets the same events and counts against the same limits.
Connect
| Setting | Value |
|---|---|
| Host | smtp |
| Port | 465 with TLS from the start, or 587 with STARTTLS |
| User name and password | A server key, sk_ or a test server’s sk_, as both |
| Authentication | PLAIN or LOGIN, once TLS is on |
The From address must be on one of the server’s verified domains. The message limits apply as they do over the API, with To, Cc and Bcc counted together.
import nodemailer from 'nodemailer';
/** Sends one message over SMTP with a server's API token as both user name and password. */
export async function sendOverSmtp(
token: string,
): Promise<string> {
const transport = nodemailer.createTransport({
host: 'smtp.sendora.se',
port: 587,
secure: false,
requireTLS: true,
auth: { user: token, pass: token },
});
const info = await transport.sendMail({
from: '"Example AB" <no-reply@example.se>',
to: 'anna@example.com',
subject: 'Your account is ready',
text: 'Hi Anna, your account at Example is ready.',
headers: {
'X-Sendora-Tag': 'welcome',
'X-Sendora-Metadata-Customer': '4711',
},
});
return info.response; // "250 2.0.0 OK: queued as <messageId>"
}
import smtplib
import ssl
from email.message import EmailMessage
# Sends one message over SMTP with a server's API
# token as both user name and password.
def send_over_smtp(token: str) -> None:
message = EmailMessage()
message["From"] = "Example AB <no-reply@example.se>"
message["To"] = "anna@example.com"
message["Subject"] = "Your account is ready"
message["X-Sendora-Tag"] = "welcome"
message["X-Sendora-Metadata-Customer"] = "4711"
message.set_content(
"Hi Anna, your account at Example is ready."
)
# Without a context of its own, starttls() would
# accept any certificate, and without a timeout
# smtplib waits forever on a stalled connection.
context = ssl.create_default_context()
with smtplib.SMTP(
"smtp.sendora.se", 587, timeout=30
) as smtp:
smtp.starttls(context=context)
smtp.login(token, token)
smtp.send_message(message)
Sendora delivers only to the envelope recipients, the addresses your client gives in RCPT TO. The To and Cc headers decide how each of them is shown, and an envelope recipient that neither header names is sent as Bcc. An address that only a header names gets nothing.
Send from Django
Django sends over SMTP out of the box and checks the server’s certificate when it starts TLS. In settings, use the server key from the environment as both user name and password. DEFAULT_ must be on a verified domain, and so must SERVER_, the address Django sends its error mail from.
import os
# In settings.py, for Django's SMTP backend, its
# default: the server key as user name and password.
EMAIL_HOST = "smtp.sendora.se"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ["SENDORA_API_TOKEN"]
EMAIL_HOST_PASSWORD = os.environ["SENDORA_API_TOKEN"]
EMAIL_TIMEOUT = 30
DEFAULT_FROM_EMAIL = "Example AB <no-reply@example.se>"
SERVER_EMAIL = DEFAULT_FROM_EMAIL
Django 6.1 replaces the EMAIL_ settings with MAILERS and refuses to start with both. On 6.1, put the same values in MAILERS["default"]["OPTIONS"] as host, port, use_, username, password and timeout. DEFAULT_ and SERVER_ stay as they are.
Set the stream, tag and metadata
| Header | Sets |
|---|---|
X-Sendora-Stream | The stream the message goes on, by its id |
X-Sendora-Tag | The message’s tag |
X-Sendora-Metadata-<key> | One metadata entry: X-Sendora-Metadata-Customer: 4711 sets { "customer": "4711" } |
Without X-Sendora-Stream, the message goes on the server’s default transactional stream.
Attachments are ordinary MIME parts. Sendora also keeps Reply-To, List-Unsubscribe, List-Unsubscribe-Post and your own X- headers. It drops other headers, such as Message-ID, Received or DKIM-Signature, and writes its own. The API refuses these headers instead of dropping them.
SMTP replies
Sendora accepts or refuses a message before the session ends. A 250 means the message is stored and already in the log, and Sendora never refuses it after that. Delivery outcomes, such as a bounce, arrive later as events. A 5xx reply means a retry will not help. A 4xx reply means a retry may succeed.
| Reply | When |
|---|---|
250 2.0.0 OK: queued as <messageId> | Accepted. The id is the one the log and the webhooks use. |
530 5.7.0 | The client sent a message without authenticating. |
535 5.7.8 | The key is wrong, revoked or an account key, or the user name and password differ. |
550 5.7.1 | The From domain is not verified, or the account is paused. On a live server, the account is not approved or without active payment. It also covers a stream in X-Sendora-Stream that is unknown, archived, paused or inbound. |
550 5.1.1 | A recipient is suppressed, or a live server was given an address at simulator. The text lists the addresses. |
550 5.6.0 | The API would refuse the message as invalid, or it breaks the rules of a broadcast stream. The text gives the reasons. |
552 5.3.4 | The message is over the size limit. |
452 4.5.3 | The message has more recipients than a message may have. Split them over several messages. |
451 4.7.0 | The per-minute limit or the monthly cap is reached. The text says when to try again. |
451 4.3.2 | Sending is disabled for everyone for the moment. |
451 4.3.0 | A temporary failure on Sendora’s side. |