The Definive Send Email Guide
Today you gonna read my Magnum Opus of sending emails. Let’s assume you want to send an email from email@example.com to firstname.lastname@example.org , 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
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
Name <email@example.com>, <firstname.lastname@example.org>, email@example.com, because@firstname.lastname@example.org 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
email@example.com and you need to find its SMTP server.
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 184.108.40.206... 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 220.127.116.11... 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
Specifically for such geniuses, you have to try MX records and even A/AAAA records again, but with
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:
ProTip! while the mandatory headers are
Toonly 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: firstname.lastname@example.org To: email@example.com Message-Id: <firstname.lastname@example.org> 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!
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 18.104.22.168... Connected to example.net. Escape character is '^]'. 220 example.net ESMTP
Now, you can send the commands.
First, Introduce yourself and be polite:
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:<email@example.com> BODY=8BITMIME SMTPUTF8.
The server will acknowledge your intention:
250 2.0.0 Roger, accepting mail from firstname.lastname@example.org
Third, clarify the recipient of that email:
The server will acknowledge that again:
250 2.0.0 I’ll make sure email@example.com 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: firstname.lastname@example.org To: email@example.com Message-Id: <firstname.lastname@example.org> Date: Sat, 15 Feb 2023 19:13:27 +0000 Subject: This is a sendable email Hi there, Did it hit the Spam folder? .
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.
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.
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.
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:<email@example.com> BODY=8BITMIME SMTPUTF8 # acknowledgment from the SMTP server 250 2.0.0 Roger, accepting mail from <firstname.lastname@example.org> # the sender tells the email's recipient RCPT TO:<email@example.com> # acknowledgment from the SMTP server 250 2.0.0 I will make sure <firstname.lastname@example.org> 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: <email@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: <firstname.lastname@example.org> # 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