1. Introduction

Although now one of the oldest electronic communication mediums, electronic mail (emails, e-mails) are still around and being used for various purposes:

Due to many of its automated applications, having a way to conveniently send mail via the command line can be essential.

In this tutorial, we’ll discusses how to use curl to send e-mails from the shell. First, we look at the curl tool in general. Next, we explore what sending mail from the command line involves. Finally, we get to different ways of sending mail using curl.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.1.4. It should work in most POSIX-compliant environments unless otherwise specified.

2. curl

The curl utility is ubiquitous when it comes to data transfer. It supports a vast amount of protocols and standards:

Let’s see an example with HTTPS:

$ curl --data-urlencode "user=USERNAME" --data-urlencode "pass=PASSWORD" https://gerganov.com/
{
  "user": "USERNAME",
  "pass": "PASSWORD"
}

In this case, we use curl to send a basic HTTPS request to https://gerganov.com, which also includes two parameters, user and pass, via the –data-urlencode option. As a result, the server returns the passed parameter and its value in a JSON format.

In fact, were we to need standard authentication, curl can facilitate that via its own –user flag:

$ curl --user USERNAME:PASSWORD https://www.gerganov.com/

These features, along with configuration options, make curl a versatile communication tool. In fact, we can also employ it for processing mail protocol messages.

3. Command-line Mail

As we already saw, curl supports most major mail protocols. Because of this, we can use curl to send mails from the terminal. Let’s see how.

3.1. Mail Exchange

To begin with, sending mail involves an actual service that can accept our request to send an e-mail and handle it appropriately. This service is usually called a mail provider or mail server and comprises several components:

  • MTA (Mail Transport Agent) via SMTP: routes and transfers mails from server to server
  • MDA (Mail Delivery Agent) via POP3 or IMAP: stores mail until a user accepts it
  • MUA (Mail User Agent) via mail client: receive mail as a user

In this context, a mail server is often synonymous with an SMTP server.

3.2. Find SMTP Server

First, we find an appropriate working SMTP server:

$ nc -v smtp.protonmail.ch 587
Connection to smtp.protonmail.ch (176.119.200.135) 587 port [tcp/submission] succeeded!
220 mailsub002.protonmail.ch ESMTP Postfix

In this case, we use netcat (nc) in [-v]erbose mode to ensure we can connect to the smtp.protonmail.ch mail server at the default port 587. We can do the same with servers like smtp.gmail.com and other default ports such as 465.

Naturally, we can host our own server locally as well.

3.3. Create an Account

Regardless of the mail server, we need to have a way to authenticate.

In many cases, this involves a username and password. To acquire these, we can either create an account in the shell of our own hosted mail server or, more commonly, register for an account with an online mail service provider like ProtonMail, Gmail, and similar.

After doing so, we usually get a set of credentials:

Critically, many providers have or had separate settings to allow direct basic authentication with a cleartext password. So, if we don’t use a self-hosted mail server, we may have to go a step further by enabling insecure access. This means enabling the server to authenticate via cleartext passwords.

However, some providers, such as Google, don’t support this old authentication method, so they have another feature.

3.4. Authentication Token

Since passing passwords around isn’t secure or in sync with best practices, passkeys or tokens, also called app passwords, come into play. These comprise data in addition to or instead of a regular password or login session that ensure only authorized users are given access.

For example, multi-factor authentication (MFA) or two-factor authentication (2FA) schemes employ such additional measures. Usually, when we configure and enable either, we get a permanent or one-time passkey token that we can use instead of a password when authentication is required. How we perform the necessary configuration varies according to the service, deployment, or provider.

In most cases, this means we have to perform at least two steps:

  1. provide a secondary means of verification for each login
  2. configure application-specific keys that we can use

After doing the above, we can use our new credentials or token when authenticating to the server of our choice for sending or receiving mail. Let’s explore how we can do this.

4. Send Mail With curl

After acquiring the necessary information, we can just issue curl commands to send emails.

In all cases, we use variables with self-explanatory names to store the relevant information. Critically, $PASS can be either a password or a preconfigured passkey or token, so $USER can be the same as or different than $SENDER_ADDRESS, depending on the service.

To get progress and error details from each command, enabling us to troubleshoot issues more easily, we can also employ the –verbose (-v) flag.

4.1. Basic Mail Sending

Let’s start off by sending a basic e-mail to a single recipient:

$ curl --ssl-reqd --url "smtps://$SERVER:$PORT" \
    --user "$USER:$PASS" \
    --mail-from "$SENDER_ADDRESS" \
    --mail-rcpt "$RECIPIENT_ADDRESS" \
    --upload-file "$MAIL_CONTENT_FILE"

For clarity, we employ the backslash to format the command on several lines.

Technically, we use several curl options:

  • –ssl-reqd ensures we use SSL/TLS for the session
  • –url specifies the URL to send requests to
  • –user (-u) specifies the credentials to use for authentication
  • –mail-from is an SMTP(S)-only option that indicates the sender of an e-mail (only last one used)
  • –mail-rcpt is an SMTP(S)-only option that indicates the recipient of an e-mail (can be specified more than once)
  • –upload-file (-T) is a way to upload a file (here, the email content)

In this case, we use $SERVER for the mail server domain name or IP address and $PORT for the respective SMTP(S) port.

Let’s explore some example values for each variable:

SERVER='smtp.example.com'
PORT='465'
USER='[email protected]'
PASS='PASSWORD'
SENDER_ADDRESS='[email protected]'
RECIPIENT_ADDRESS='[email protected]'
MAIL_CONTENT_FILE='mailfile'

Finally, $MAIL_CONTENT_FILE is a path to a file that contains the message text. Alternatively, we can pass this data via stdin by using a dash instead of a file name or directly leveraging here-strings.

4.2. Mail Content

The $MAIL_CONTENT_FILE file or standard input can just contain a simple text message representing the mail body, as we can see with cat:

$ cat mailfile
This is the body of the e-mail.

However, we can also prepend colon-separated headers on different lines according to SMTP:

HEADER_NAME: HEADER_VALUE

There are a number of standard headers, also known as header fields, some of which are more common than others:

  • From: precedes a NAME <ADDRESS> combination of the sender in the given format
  • To: precedes a NAME <ADDRESS> combination of the recipient in the given format
  • Cc: precedes a NAME <ADDRESS> combination of a further public recipient in the given format
  • Reply-To: precedes a NAME <ADDRESS> combination of the account to reply to in the given format
  • Subject: precedes the SUBJECT TITLE

If we omit a recipient in the headers, but add it as –mail-rcpt, we end up with a blind carbon copy, i.e., a recipient hidden from other recipients. The general structure above is defined in RFC 5322 – Internet Message Format. Each header uses the ASCII encoding. However, the actual body can employ different MIME formats, not only text/plain.

Let’s see a more complex and comprehensive example of an email:

$ cat mailfile
From: "Hiks Gerganov" <[email protected]>
To: "Michał Aibin" <[email protected]>
Subject: Basic Message
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable

<b>Bolded text</b>

Now, let’s use this file with our earlier syntax:

$ MAIL_CONTENT_FILE=mailfile
$ curl --ssl-reqd --url "smtps://$SERVER:$PORT" \
    --user "$USER:$PASS" \
    --mail-from "$SENDER_ADDRESS" \
    --mail-rcpt "$RECIPIENT_ADDRESS" \
    --upload-file "$MAIL_CONTENT_FILE"

Importantly, if $SENDER_ADDRESS differs from the addresses in From:, servers might reject our mail due to obfuscation.

4.3. curl Headers

For all headers we specify in the mail file, we can also use the –header (-H) option of curl directly.

For example, let’s create a Bash script called xsendmail.sh to demonstrate this:

$ cat xsendmail.sh
#!/usr/bin/env bash

SERVER='smtp.example.com'
PORT='465'
USER='[email protected]'
PASS='PASSWORD'
SENDER_ADDRESS="$USER"
SENDER_NAME='SENDER'
RECIPIENT_NAME='y'
RECIPIENT_ADDRESS='[email protected]'
SUBJECT='Scripted Mail'
MESSAGE=$'Line 1\nLine 2'

curl --ssl-reqd --url "smtps://$SERVER:$PORT" \
    --user "$USER:$PASS" \
    --mail-from "$SENDER_ADDRESS" \
    --mail-rcpt "$RECIPIENT_ADDRESS" \
    --header "Subject: $SUBJECT" \
    --header "From: $SENDER_NAME <$SENDER_ADDRESS>" \
    --header "To: $RECIPIENT_NAME <$RECIPIENT_ADDRESS>" \
    --form '=(;type=multipart/mixed' --form "=$MESSAGE;type=text/plain" --form '=)'

Here, we used the –form (-F) option to specify a given format for the mail body in $MESSAGE. In this case, $MESSAGE uses ANSI-C quoting to insert newlines and other escape characters.

At this point, we can make the script executable via chmod and run it:

$ chmod +x xsendmail.sh
$ ./xsendmail.sh

Of course, we can use command-line arguments or basic read prompts for any and all parameters, including attachments.

4.4. Mail With Attachment

In addition to data and metadata, we can also add attachments to emails:

$ cat xsendmail.sh
#!/usr/bin/env bash

SERVER='smtp.example.com'
PORT='465'
USER='[email protected]'
PASS='PASSWORD'
SENDER_ADDRESS="$1"
SENDER_NAME='SENDER'
RECIPIENT_NAME='y'
RECIPIENT_ADDRESS='[email protected]'
SUBJECT='Scripted Mail'
MESSAGE=$'Line 1\nLine 2'
ATTACHMENT_FILE='/home/user/att1'
ATTACHMENT_TYPE="$(file --mime-type '$ATTACHMENT_FILE' | sed 's/.*: //')"

curl --ssl-reqd --url "smtps://$SERVER:$PORT" \
    --user "$USER:$PASS" \
    --mail-from "$SENDER_ADDRESS" \
    --mail-rcpt "$RECIPIENT_ADDRESS" \
    --header "Subject: $SUBJECT" \
    --header "From: $SENDER_NAME <$SENDER_ADDRESS>" \
    --header "To: $RECIPIENT_NAME <$RECIPIENT_ADDRESS>" \
    --form '=(;type=multipart/mixed' --form "=$MESSAGE;type=text/plain" 
    --form "file=@$ATTACHMENT_FILE;type=$ATTACHMENT_TYPE;encoder=base64" --form '=)'

By modifying our earlier script, we made several additions:

  • $ATTACHMENT_FILE sets the path of a file to attach
  • $ATTACHMENT_TYPE uses a pipeline with the file command and sed to extract only the MIME encoding of the attachment
  • –form “file=@$ATTACHMENT_FILE;type=$ATTACHMENT_TYPE;encoder=base64” adds the file with its type and encodes everything in base64 to avoid syntax issues

Importantly, it’s vital to ensure the attachment file exists, as the ATTACHMENT_TYPE assignment operation might otherwise fail and cause a syntax problem. As a result, the mail might not get sent and we can end up with an unexpected error.

5. Summary

In this article, we explored the requirements and methods for sending e-mails via curl.

In conclusion, although the core concept is fairly basic, sending mail from the command line may involve a number of configuration options on both the server and client side.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.