OquMail
Loading account…
Email API3 min readUpdated

Send Email from Python: requests, smtplib or Django Settings

Send email from Python three ways: one requests call to the API, smtplib with STARTTLS on port 587, or the Django settings block. Errors and attachments too.

Python can send email three ways, and the right one depends on where the code runs. A web app or a worker should call the HTTPS API with requests: no ports, no TLS choices, and every send appears in a delivery log. A standalone script that already has a mailbox password can use smtplib with STARTTLS. A Django project should just fill in the settings block and keep using send_mail. Here are all three, with the errors each one produces and how to attach a file.

Quick answer

For apps: requests.post to the send endpoint with a Bearer key, a JSON body where to is a list, and a 10-second timeout; 202 means queued. For scripts: smtplib.SMTP to mail.oqumail.com on port 587, then starttls(), then login with the full mailbox address. For Django: EMAIL_HOST=mail.oqumail.com, EMAIL_PORT=587, EMAIL_USE_TLS=True, and the mailbox as EMAIL_HOST_USER and DEFAULT_FROM_EMAIL.

Option 1: the API with requests

Create an API key on the mailbox you want to send from and put it in the environment as OQUMAIL_API_KEY. The From address is that mailbox; the body only carries an optional fromName:

import os
import requests

API = "https://api.oqumail.com/api/v1/emails"

def send_email(to: str, subject: str, text: str, html: str | None = None) -> str:
    body = {"to": [to], "subject": subject, "text": text, "fromName": "Acme"}
    if html:
        body["html"] = html
    r = requests.post(
        API,
        headers={"Authorization": f"Bearer {os.environ['OQUMAIL_API_KEY']}"},
        json=body,
        timeout=10,
    )
    if r.status_code != 202:
        raise RuntimeError(f"send failed: {r.status_code} {r.text}")
    return r.json()["id"]  # keep this next to your own record for support

send_email("customer@example.com", "Your receipt", "Thanks for your order.")

Option 2: smtplib for scripts and cron

For a nightly report or a monitoring script on a server, smtplib with the mailbox login is fine. Port 587 with starttls(); SMTP_SSL and port 465 are the wrong pair here:

import os
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "alerts@yourdomain.com"
msg["To"] = "ops@yourdomain.com"
msg["Subject"] = "Nightly report"
msg.set_content("Plain-text body for clients that do not render HTML.")
msg.add_alternative("<p>HTML body.</p>", subtype="html")

with smtplib.SMTP("mail.oqumail.com", 587, timeout=20) as s:
    s.starttls()
    s.login("alerts@yourdomain.com", os.environ["MAILBOX_PASSWORD"])
    s.send_message(msg)

Option 3: Django settings

Django sends through its SMTP backend, so the whole change is in settings.py. After that send_mail, password resets and admin error emails all go out from your domain:

# settings.py
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "mail.oqumail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True        # STARTTLS on 587; leave EMAIL_USE_SSL at False
EMAIL_HOST_USER = "app@yourdomain.com"
EMAIL_HOST_PASSWORD = os.environ["MAILBOX_PASSWORD"]
EMAIL_TIMEOUT = 20
DEFAULT_FROM_EMAIL = "app@yourdomain.com"
SERVER_EMAIL = "app@yourdomain.com"

# test from the shell:
#   python manage.py sendtestemail you@yourdomain.com

Free business email on your own domain

OquMail gives you up to 15 mailboxes on your domain — free — with guided SPF/DKIM/DMARC, webmail, IMAP/SMTP for any mail app, and a send API. Setup is a few DNS records; changes usually take effect within minutes, occasionally up to 48 hours. Start at oqumail.com.

Get started free

FastAPI, Flask and Celery

In a request handler, do not block the response on the send. FastAPI has BackgroundTasks: background_tasks.add_task(send_email, user.email, "Welcome", text). Flask apps usually hand the call to a Celery or RQ worker. Either way the function above is what the worker runs, and the API option is the better fit for workers because a failed send comes back as a status code you can retry on rather than a half-open SMTP session.

The errors you will see

  • API 400: to was sent as a string, or subject and both bodies are missing. Wrap the address in a list.
  • API 401: the key is wrong, revoked, or the environment variable is not set in that process. Print whether os.environ has the key, never the key itself.
  • API 429: the per-minute or daily limit for the key. Read the Retry-After header and back off instead of retrying in a loop.
  • smtplib SSLError or "wrong version number": SMTP_SSL or port 465 was used. Use SMTP on 587 and call starttls().
  • smtplib SMTPAuthenticationError: the mailbox password changed, or the username is not the full address.
  • Hangs with no error: no timeout was set. Always pass timeout= to requests and smtplib.

Common questions

How do I attach a file?

With the API, add an attachments list to the body: [{"filename": "report.pdf", "contentType": "application/pdf", "contentBase64": base64.b64encode(data).decode()}]. With smtplib, call msg.add_attachment(data, maintype="application", subtype="pdf", filename="report.pdf") before sending.

Can I use httpx or aiohttp instead of requests?

Yes. The request is a plain POST with a JSON body and a Bearer header; any HTTP client works, and an async client fits naturally in FastAPI.

How do I know a message was delivered?

API sends appear in the delivery log in the OquMail dashboard with the receiving server's response. For smtplib, a clean return from send_message means the relay accepted it, and the same delivery log shows what happened next.