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:

  1. Resolve recipient SMTP server
  2. Prepare the email itself
  3. Send it
  4. 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 and To 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