The Definive Send Email Guide
Yā, min’na.
Today you gonna read my Magnum Opus of sending emails. Let’s assume you want to send an email from sender@example.com to recipient@example.net , that guide will cover communication between SMTP servers (MTA to MTA ), moreover, for the sake of simplicity the examples will use generic tools instead of some specific programming language snippets.
So, we’re gonna send the following message from sender@example.com
to recipient@example.net
:
Hi there,
Did it hit the Spam folder?
Here is the high-level algorithm of all actions:
- Resolve recipient SMTP server
- Prepare the email itself
- Send it
- Show the results.
Resolve recipient SMTP server#
In the How to validate an email address post I described how to check that email really exists and in fact, we need to do the same here.
ProTip!: never parse the email address yourself, if there are proper alternatives available, otherwise you will have a lot of fun time parsing things like
undisclosed-recipients:;
,Name <email@example.com>, <email@example.com>, email@example.com, because@i-can@example.com
and other valid-but-tricky things. The best you can do is to use your programming language’s email address parser, like Go’s mail.ParseAddress
After you followed the ProTip, you know that the real email address is recipient@example.net
and you need to find its SMTP server.
MX records#
Any respectable email server must have correct MX record(-s) to be discoverable without hacks and workarounds (if yours doesn’t - find something else, like my personal favorite - migadu , not an ad, they don’t pay me money, in fact, I pay them and pretty happy with that).
So, what are you gonna do? MX Lookup!
$ dig MX example.net
example.net. 300 IN MX 20 aspmx2.migadu.com.
example.net. 300 IN MX 10 aspmx1.migadu.com.
Ok, you have them! Don’t be happy at this point, tho, they may not work (and it will be covered in the next section).
You can perform SMTP requests using simple telnet and find out that one of the records you found responds properly:
$ telnet aspmx1.migadu.com 25
Trying 37.59.50.128...
Connected to aspmx1.migadu.com.
Escape character is '^]'.
220 aspmx1.migadu.com ESMTP
Lucky you! You skip the next section and move to the “prepare the email itself”.
ProTip! that’s normal to have multiple MX records at once, because sometimes servers may be down, so you need to try another MX record. Another option is deliberately invalid entry in the MX records, the infamous poor man’s greylisting - nolisting . So, if one of the MX records didn’t work - try others, one by one
A/AAAA or MX records didn’t work#
Missing or broken MX records? Not the rarest event in the universe, IMO. So, what can you do in that case? Just use the A/AAAA record, what else can you do?
According to the RFC5321
, you have to try to communicate with the host itself (usually, it’s the @
A or AAAA record) before giving up completely.
$ telnet example.net 25
Trying 128.50.59.37...
Connected to example.net.
Escape character is '^]'.
220 example.net ESMTP
Hooray, you did that! Now, move to the next section - prepare the email itself.
A/AAAA record didn’t work, either.#
Non-compliance happens, even with the best of us.
Did you know, that you have to accept incoming mail from MTAs on the 25
port? Yeah? Cool! But looks like example.net
’s postmaster didn’t, like some other postmasters who accept incoming mail from other MTAs on 465
or 587
ports.
Specifically for such geniuses, you have to try MX records and even A/AAAA records again, but with 465
and 587
ports this time.
Of course, that’s completely optional, because if your email server accepts incoming mail from MTAs on non-25
port only, you have to shut it down and be ashamed of yourself.
Prepare the email itself#
At this point you know for sure where to send the email, but not what to send and how to send it. So, let’s focus on the what part.
Any email is a message itself plus tons of metadata, but there are only a few really necessary metadata fields. In the How to Send Email (into Spam) I already covered the needed headers, here they are:
- MIME-Version
- Content-Type
- Content-Transfer-Encoding
- From
- To
- Message-Id
- Date
- Subject
ProTip! while the mandatory headers are
From
andTo
only and any modern email server will be able to deliver such email (yeah, even without subject and text), in 99.99999% they will drop it completely and it won’t reach even the recipient’s Spam folder. So, to be somewhat trustworthy (and reach the Spam folder), you’d better send all the headers from the list above.
So, using that knowledge we can compose The sendable email, like this:
MIME-Version: 1.0
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: 8BIT
From: sender@example.com
To: recipient@example.net
Message-Id: <some-random-string@example.com>
Date: Sat, 15 Feb 2023 19:13:27 +0000
Subject: This is a sendable email
Hi there,
Did it hit the Spam folder?
Congratulations! Now you have an email. Like, a real, sendable, and readable one. GoTo the next section: Send it!
Send it#
Ok, you have what to send, you have where to send and now you need to find out how to send it.
So, how? Using SMTP commands, beep-boop!
You can interact with the recipient’s SMTP server over Simple is a lie Mail Transfer Protocol over telnet.
To do that, you need to use special SMTP commands that will tell the recipient’s SMTP server what you want.
That topic was partially covered in the How to validate an email address
post, but now I’ll show you the full cycle.
So, you connected to the recipient’s SMTP server using telnet:
$ telnet example.net 25
Trying 128.50.59.37...
Connected to example.net.
Escape character is '^]'.
220 example.net ESMTP
Now, you can send the commands.
First, Introduce yourself and be polite: EHLO example.com
.
The server will respond to your greeting and send you a multi-line list of supported extensions, e.g.:
250-Hello example.com
250-PIPELINING
250-8BITMIME
250-ENHANCEDSTATUSCODES
250-CHUNKING
250-STARTTLS
250-AUTH PLAIN
250-SMTPUTF8
Second, tell the recipient’s SMTP server that you want to send the email: MAIL FROM:<sender@example.com> BODY=8BITMIME SMTPUTF8
.
The server will acknowledge your intention:
250 2.0.0 Roger, accepting mail from sender@example.com
Third, clarify the recipient of that email: RCPT TO:<recipient@example.net>
.
The server will acknowledge that again:
250 2.0.0 I’ll make sure recipient@example.net gets this
Now, you can tell the server you want to actually send an email, and use the DATA
command (without args).
The server will acknowledge that:
354 2.0.0 Go ahead. End your data with .
And now, the magic happens - you can send the email without any additional commands:
MIME-Version: 1.0
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: 8BIT
From: sender@example.com
To: recipient@example.net
Message-Id: <some-random-string@example.com>
Date: Sat, 15 Feb 2023 19:13:27 +0000
Subject: This is a sendable email
Hi there,
Did it hit the Spam folder?
.
Success#
And the server will acknowledge that:
250 2.0.0 OK: queued
Congratulations, you did it!
Now, you can finally show Email has been sent
notification to your user.
BUT! Server can reject your email, you know, so let’s see what potentially can happen.
“Permanent” Failure#
ProTip! despite SMTP has 4xx status codes for temporary issues and 5xx status codes for permanent issues, you should consider all of them as temporary, because according to Wikipedia good part of the “permanent” issues can be fixed after some time, so the best you can do is to retry delivery after some time.
“Permanent” in double quotes because in most cases it’s not actually permanent, but something is not right and there is no clear understanding when it will be fixed. So, if you try again in a day, here is an example of not-so-permanent error response:
552 Requested mail action aborted: exceeded storage allocation
It’s just no space left in the recipient’s mailbox storage and when the recipient will clear all the spam they have, the new spam (including your email) will go through.
Temporary Failure#
Oh, this section is my favorite - in 99% cases 4xx
response code means that you was graylisted.
You don’t know what graylisting means? Go check out the Other Annoyances post
, where I described all those shady techniques and whined a lot due to high level of annoyances.
Bonus#
The real email with real SMTP communication was composed and sent during the writing of that article. Of course, the server name and emails were replaced with the example values.
Both the sender SMTP program and the recipient SMTP server are instances of Postmoogle
Click here to show full log with comments
# connected to the SMTP server, it responds:
220 example.net ESMTP Service Ready
# introduction from the sender
EHLO example.com
# acknowledgment from the SMTP server
250-Hello example.com
250-PIPELINING
250-8BITMIME
250-ENHANCEDSTATUSCODES
250-CHUNKING
250-STARTTLS
250-AUTH PLAIN
250-SMTPUTF8
# sending the size of the email to the SMTP server (there may be limits, you know)
250 SIZE 1073741824
# the sender program found out that the SMTP server supports STARTTLS and decided to start encrypted communication.
# To do so, the sender program sends the `STARTTLS` command and re-starts the connection to the SMTP server, this time using
# the SMTP server's TLS certificate to encrypt communications
STARTTLS
# server acknowledges that
220 2.0.0 Ready to start TLS
# and it starts again, this time using encrypted transport
EHLO example.com
250-Hello example.com
250-PIPELINING
250-8BITMIME
250-ENHANCEDSTATUSCODES
250-CHUNKING
250-AUTH PLAIN
250-SMTPUTF8
250 SIZE 1073741824
# here we are - the sender tells that it wants to send an email
MAIL FROM:<sender@example.com> BODY=8BITMIME SMTPUTF8
# acknowledgment from the SMTP server
250 2.0.0 Roger, accepting mail from <sender@example.com>
# the sender tells the email's recipient
RCPT TO:<recipient@example.net>
# acknowledgment from the SMTP server
250 2.0.0 I will make sure <recipient@example.com> gets this
# the sender tells it's ready to actually send the email
DATA
# acknowledgment from the SMTP server
354 2.0.0 Go ahead. End your data with <CR><LF>.<CR><LF>
# aaand here we are - the email!
# as you may notice, there are a lot of other headers, apart from the strictly necessary
# that's because the email is signed with the DKIM key (like a PGP signature) by the sender's server
DKIM-Signature: a=rsa-sha256; bh=6wcWvFNLGVP7s6xc2M6Q1wF+w+Hte7vwBo9xm6y1zEA=;
c=simple/simple; d=example.com;
h=Content-Type:Date:From:Message-Id:Mime-Version:References:Subject:To;
s=postmoogle; t=1676477802; v=1;
b=IrUwXa1YOePwZAn1IFG/4MDa+OFVjHXJ+s0Nx0zykM0UhdNe++OqR51VgW6jAoRZw99YCpJ3
ZFVksSjPjCbNmZW2oJew/hdoZeuNXFymekr+t4ge82dqIX/GBLrP0Sw0TiFR70pq5ZFMfqmbkjz
pXu/lAavDC3DLdvo8Qa3bXxqDO7C9RFBNSX7QOB4gI6nLOt0KY8u3FEKoiy4hFK1sUYHSe2wEjS
OtjwZwVAC500t7qDHH7Ef76VOzoa5pvhxn4p+6Ohua/t6Vkwu7KKPtSzrS8AAGPE6OyihKckHeE
fo2AUKUiyi9mexkeqCCJ6szhpiN67+DghB6axXn5R1eukWIaWow9M+qbAQAALrtT+ILg57ERwgo
S9koaIOS2Q2MQ4eFCbxdTDuul4Y+PrChxVKwMqfdMAt4iyZf5+PUU75DsKtB9YmHsXwmHOGNCmr
/62rKqgji4cQ8QI7TfwJ278t4ja/3bGsrNDvv+BFjNbEacNEw6P9W9SD2CDjlWSZ9
# as you already noticed, the sender is Postmoogle, as well as the recipient's SMTP server,
# postmoogle is a polite SMTP program, thus it will try to do its best and send a multipart email,
# containing both HTML and plaintext representations of the same content, so recipients can
# read it in a way they prefer.
Content-Type: multipart/alternative;
boundary=enmime-53fa6fb1-89c8-4174-9f4a-e21cacdb93af
# Real date, btw
Date: Wed, 15 Feb 2023 18:16:42 +0200
From: <sender@example.com>
# and message ID as well. Well, the host was replaced.
Message-Id: <$Z-A4VqoBItH4DOWhQwXGfie7_7dCjywKtvWZQGyeA4g@example.com>
# be polite
Mime-Version: 1.0
# well, too polite, IMO
References: <$Z-A4VqoBItH4DOWhQwXGfie7_7dCjywKtvWZQGyeA4g@local.host>
# yup-yup
Subject: This is a sendable email
To: <recipient@example.net>
# the plaintext part
--enmime-53fa6fb1-89c8-4174-9f4a-e21cacdb93af
Content-Type: text/plain; charset=utf-8
Hi there,
Did it hit the Spam folder?
# the HTML part
--enmime-53fa6fb1-89c8-4174-9f4a-e21cacdb93af
Content-Type: text/html; charset=utf-8
Hi there,<br>
Did it hit the Spam folder?
--enmime-53fa6fb1-89c8-4174-9f4a-e21cacdb93af--
# and now, send the special signal to end the email transfer
.
# server acknowledges it and delivers it to the recipient!
250 2.0.0 OK: queued