Skip to main content

mailsenpai (Python)

Python client for SMTP Senpai by MailSenpai, the EU-hosted SMTP relay and transactional email API. It has no runtime dependencies (standard library only) and includes:

  • a typed, synchronous API client: send, account status, event log (with a cursor iterator), suppression list, webhook settings;
  • webhook signature verification;
  • a Django email backend: mailsenpai.django.EmailBackend;
  • a CrewAI tool: mailsenpai.crewai.SendEmailTool (optional extra).

Docs: https://en.mailsenpai.com/smtp-api/ · Product: https://en.mailsenpai.com/smtp-senpai/ · All integrations: https://en.mailsenpai.com/integrations/ Source code and issues: https://gitlab.com/smtp-senpai/mailsenpai-python

Install

pip install mailsenpai              # client + Django backend (Django itself not included)
pip install "mailsenpai[crewai]"    # adds CrewAI

Python 3.9+. Get your API key (it starts with msp_) in the MailSenpai customer area, SMTP Senpai page. Call the API from your server only: it does not accept browser requests.

Send an email

from mailsenpai import MailSenpai, Attachment

client = MailSenpai("msp_...")          # or set MAILSENPAI_API_KEY

result = client.send(
    to="customer@example.com",          # or a list of up to 50 addresses: each gets its own copy
    from_email="orders@yourdomain.com", # must be on a domain verified in your account
    from_name="Your shop",
    subject="Order 10293 confirmed",
    html="<p>Thanks, your order is confirmed.</p>",
    text="Thanks, your order is confirmed.",
    reply_to="support@yourdomain.com",
    headers={"X-Order": "10293"},       # only X-... headers are kept
    attachments=[Attachment.from_path("invoice.pdf")],  # up to 10 files, 10 MB in total
    idempotency_key="order-10293",      # same key within 24h = no second send
)

print(result.sent, result.message_id, result.remaining)
for r in result.not_sent:               # e.g. recipients on the suppression list
    print(r.email, r.reason or r.error)

SendResult normalises single and multi-recipient responses: results (one RecipientResult per address), sent, sent_count, message_id, message_ids, remaining, over_plan, replayed and the raw JSON.

Account, log and suppression list

client.status()                        # SMTP details, monthly volume, tracking, suppressed count
client.stats(days=7)                   # counts and delivery/bounce/complaint rates

page = client.events(limit=50, type="bounce")   # latest events, newest first

# Read everything new since the last event you processed (oldest first, follows the cursor)
for event in client.iter_events(after=last_event_id):
    handle(event)
    last_event_id = int(event["event_id"])

client.suppressions(search="example.com")       # SuppressionList(total, items)
client.suppress("someone@example.com", note="asked to stop")
client.suppress("@competitor.example")          # a whole domain
client.unsuppress("someone@example.com")        # only entries you added yourself

Event types: sent, delivered, bounce, defer, complaint, dropped, open, click (open and click need tracking on). Dates are UTC (YYYY-MM-DD HH:MM:SS).

Errors

Every error subclasses mailsenpai.MailSenpaiError and has message (the API text, in Italian), status_code and body:

Exception When
BadRequestError (400) missing or invalid data
AuthenticationError (401) key missing or not valid
PermissionDeniedError (403) sender domain not verified, account suspended or being activated
NotFoundError (404) entry not found
ConflictError (409) same Idempotency-Key in progress; suppression entry added by a bounce/complaint
PayloadTooLargeError (413) attachments over 10 MB
UnprocessableEntityError (422) Idempotency-Key reused with different data
RateLimitError (429) monthly volume used up or spending cap reached
UpstreamError (502) the sending server did not accept the message
APIConnectionError network problem, timeout

The client does not retry on its own: if you retry a send, pass an idempotency_key.

Webhooks

Set the webhook once (client.set_webhook("https://yoursite.example/hooks/mailsenpai", ["delivered", "bounce", "complaint"])) and keep the returned segreto (whsec_...). Verify every call on the raw body:

from mailsenpai import verify_webhook, WebhookVerificationError

def mailsenpai_webhook(request):               # Django example
    try:
        event = verify_webhook(
            request.body,
            request.headers.get("X-MailSenpai-Signature"),
            settings.MAILSENPAI_WEBHOOK_SECRET,
        )                                      # 5 minutes tolerance by default
    except WebhookVerificationError:
        return HttpResponse(status=400)
    if event["type"] == "bounce":
        ...
    return HttpResponse(status=204)

The signature is t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>. Answer 2xx quickly: failed deliveries are retried for about 24 hours.

Django

# settings.py
EMAIL_BACKEND = "mailsenpai.django.EmailBackend"
MAILSENPAI_API_KEY = "msp_..."   # or the MAILSENPAI_API_KEY environment variable
# optional: MAILSENPAI_TIMEOUT = 30

With Django 6.1+ you can use MAILERS instead:

MAILERS = {
    "default": {
        "BACKEND": "mailsenpai.django.EmailBackend",
        "OPTIONS": {"api_key": "msp_..."},
    },
}

Then use Django as usual (send_mail, EmailMessage, EmailMultiAlternatives). Supported: text and HTML (attach_alternative(html, "text/html") or content_subtype = "html"), attachments, reply_to, custom X-... headers (other headers are dropped by the API), display names in from_email.

Good to know:

  • every recipient (To, Cc and Bcc) gets an individual copy, so Cc addresses are not visible to the other recipients; use SMTP if you need a visible Cc;
  • messages with more than 50 recipients are split into several API calls;
  • inline images (Content-ID) are sent as regular attachments;
  • after sending, message.mailsenpai_message_ids and message.mailsenpai_results hold the outcome;
  • recipients on the suppression list are not an error: they are logged as a warning on the mailsenpai logger.

Tested with Django 5.2 LTS and 6.1.

CrewAI

from crewai import Agent
from mailsenpai.crewai import SendEmailTool

email_tool = SendEmailTool(
    from_email="assistant@yourdomain.com",   # or MAILSENPAI_FROM_EMAIL
    from_name="Your company",
    allowed_recipients=["@yourdomain.com", "partner@example.com"],  # optional safety net
)

agent = Agent(role="Support assistant", goal="...", backstory="...", tools=[email_tool])

The agent fills to, subject, body, is_html and reply_to. The tool returns a short text with the outcome (errors are returned as text so the agent can react). The key comes from api_key= or MAILSENPAI_API_KEY. Use allowed_recipients to make sure an agent can only email the people you expect.

SMTP

If you prefer SMTP: host relay.mailsenpai.com, port 2525, STARTTLS, username and password from the customer area (client.status()["smtp"] shows host, port and username).

Development

pip install -e ".[test]" django
pytest
python -m build

License

MIT, © MailSenpai.

Metadata

Release files for mailsenpai 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mailsenpai 1.0.0
File Size Uploaded
mailsenpai-1.0.0.tar.gz 24.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mailsenpai 1.0.0
File Interpreter ABI Platform
mailsenpai-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 44.8 kB

Release files / mailsenpai-1.0.0.tar.gz

Download URL mailsenpai-1.0.0.tar.gz
Size 24.7 kB
Tags Source
SHA-256 checksum
How to use checksums
847995127bae69e332ea438966faa9b5dde32ffc65107132e1a640a98f93dca5
BLAKE2b-256 checksum
How to use checksums
7927f656be72cc41a3ba241c81987ac1fb5f3bcc781ddcb2a66ed47cf0c4732a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / mailsenpai-1.0.0-py3-none-any.whl

Download URL mailsenpai-1.0.0-py3-none-any.whl
Size 20.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bdb162cd1fe45309d22ea2f33390c9400394e505958601a71400e520e06dfa8e
BLAKE2b-256 checksum
How to use checksums
fb0f9c8ab898f241c0249b36bf189475ea618a8434d952fc28b75235801eb74f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page