Sending Email From Your App With KaiMail SMTP (Python, Ruby and PHP Samples)


You have a side project that needs to send a few emails from your custom domain. A signup confirmation, a password reset, the occasional notification. Nothing fancy. You do not want to embed your personal Gmail credentials in the code, you do not want to set up a full transactional email account for 200 emails a month, and you already have a KaiMail account doing your inbound forwarding.

Good news. You can use the same KaiMail account to send email via our SMTP functionality. This guide shows you how, in Python, Ruby and PHP, with the smallest amount of code I could get away with.

What You Need

  1. A paid KaiMail plan. SMTP sending is included on Plus, Pro, and Business (see the announcement post for the rationale).
  2. A custom domain on KaiMail with DKIM and SPF records configured. If you have not done the DKIM step yet, the DKIM setup guide walks through it for Route 53; the same record works on any DNS provider.
  3. Your SMTP credentials. Log in to the dashboard, go to your profile page, and copy the SMTP username and SMTP password. The SMTP password is separate from your dashboard login password on purpose, so you can rotate it without locking yourself out of the dashboard.

Connection Settings

Setting Value
SMTP Server mail.kaimail.net
Port 587
Security STARTTLS
Username Your KaiMail email
Password Your SMTP password

This is plain SMTP submission. Nothing proprietary. Any reasonable SMTP library in any language will work. The two examples below use the most ordinary tools you would reach for in Python and Ruby.

Sending From Python

Python's standard library has everything you need. No pip install required.

import os
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg["Subject"] = "Hello from KaiMail"
msg.set_content("Sent via KaiMail SMTP.")

with smtplib.SMTP("mail.kaimail.net", 587) as smtp:
    smtp.starttls()
    smtp.login(os.environ["KAIMAIL_USER"], os.environ["KAIMAIL_PASS"])
    smtp.send_message(msg)

Save it, export KAIMAIL_USER and KAIMAIL_PASS in your shell, and run it. That is the whole thing.

Two details that bite people the first time:

  1. Call starttls() before login(). If you call them in the other order, you are sending your password over an unencrypted connection. KaiMail will reject the login, but you should not rely on the server to save you from that mistake. Always upgrade the connection first.
  2. The From address must be a domain you own on KaiMail. The server validates this on every send. If you try to send as [email protected] or as a domain you have not added to your account, the send is rejected. This is the feature, not the bug. It is how we stop your account from being used to spoof other people.

If you want HTML email, use msg.add_alternative(html, subtype="html") after set_content(). The rest of the code stays the same.

Sending From Ruby

Ruby's standard library does have net/smtp, but the mail gem is the de-facto choice and it is much less awkward to use. One dependency is a fair price.

require "mail"

Mail.defaults do
  delivery_method :smtp,
    address: "mail.kaimail.net",
    port: 587,
    user_name: ENV["KAIMAIL_USER"],
    password: ENV["KAIMAIL_PASS"],
    authentication: :plain,
    enable_starttls_auto: true
end

Mail.deliver do
  from    "[email protected]"
  to      "[email protected]"
  subject "Hello from KaiMail"
  body    "Sent via KaiMail SMTP."
end

gem install mail, set the same two environment variables, and run it.

The Ruby gotchas are essentially the same ones as Python, just with different keyword names:

  1. enable_starttls_auto: true is doing the work smtp.starttls() did in Python. Without it, the gem will happily try to send your credentials in the clear.
  2. authentication: :plain is correct here. STARTTLS handles the encryption; PLAIN auth runs over the encrypted channel. Do not switch to :login or :cram_md5 looking for "more security". You will not find it.
  3. The same From validation applies. A domain you do not own on KaiMail will fail.

If you are inside Rails, you can drop the Mail.defaults block straight into config/environments/production.rb as config.action_mailer.smtp_settings. ActionMailer uses the same gem under the hood, so the keys are identical.

Sending From PHP

PHP's built-in mail() function is famously painful for SMTP. It expects a configured local sendmail, it does not handle authentication, and the error reporting is minimal. Skip it. Use PHPMailer, which is the de-facto choice and has been for a decade.

<?php
require "vendor/autoload.php";

use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host       = "mail.kaimail.net";
$mail->Port       = 587;
$mail->SMTPAuth   = true;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Username   = getenv("KAIMAIL_USER");
$mail->Password   = getenv("KAIMAIL_PASS");

$mail->setFrom("[email protected]");
$mail->addAddress("[email protected]");
$mail->Subject = "Hello from KaiMail";
$mail->Body    = "Sent via KaiMail SMTP.";

$mail->send();

composer require phpmailer/phpmailer, set the same two environment variables, and run it with php send.php.

The PHP gotchas:

  1. SMTPSecure = ENCRYPTION_STARTTLS is the equivalent of starttls() in Python. The other constant is ENCRYPTION_SMTPS, which is implicit TLS on 465. We do not run 465, so do not use that one.
  2. new PHPMailer(true) with the boolean argument turns on exceptions. Without it, errors are silently stored on the object and send() just returns false. You will lose hours debugging "why did nothing happen" the first time. Always pass true.
  3. The same From validation applies. Spoofed sender domains are rejected by the server.

If you are inside Laravel, you do not need PHPMailer at all. Symfony Mailer ships with the framework; set MAIL_MAILER=smtp, MAIL_HOST=mail.kaimail.net, MAIL_PORT=587, MAIL_ENCRYPTION=tls, and MAIL_USERNAME / MAIL_PASSWORD in your .env, and Laravel does the rest.

Common Gotchas

A few more things worth flagging, drawn from actual support tickets:

  1. The SMTP password is not your dashboard password. Two different secrets, two different rotation cycles. If your code stops authenticating, regenerate the SMTP password from the profile page; do not change your account password.
  2. The From domain must be one you own on KaiMail. The server rejects spoofed senders. Adding a new domain in the dashboard is a one-minute job, but you do have to do it before you can send from it.
  3. Use STARTTLS on 587, not implicit TLS on 465. KaiMail does not run a port 465 listener. If your library defaults to 465 (some Ruby, PHP, and Java configurations do), set the port explicitly.
  4. The quota is a rolling 30-day window, not a calendar month. A burst of welcome emails on the first of the month does not reset on the first of the next month. Watch the usage bar in the dashboard so you are not surprised.

Where to Go Next

This script is the smallest thing that works. For production code, three additions are worth doing:

  1. Send through a queue. SMTP sends are blocking and slow. Hand the send off to Sidekiq, Celery, or a background job and let your request handlers return fast.
  2. Retry on transient failures. SMTP responses in the 4xx range are transient (greylisting, temporary load). 5xx are permanent (bad address, policy rejection). Retry the first; do not retry the second.
  3. Log the message ID returned by the SMTP server. When a customer asks "did your system send the email?", the message ID is what you grep for in the dashboard's sent log.

If you also need to receive email and pipe it into your application, the webhook integration guide is the companion piece to this one. Together they cover both directions.

For the full feature description, see the SMTP sending feature page. If you have not enabled SMTP on your account yet, the original announcement has the dashboard walkthrough. And if you are still on the "Send mail as" Gmail workaround, the setup guide for that explains why programmatic SMTP is the cleaner option once you are writing code anyway.