Skip to content
 

Send email with attachment(s) from script or command line

Since every time I have to do this (which is "not very often") I have to look it up, google for it, etc., let's summarize it here once and for all. The problem is not sending the email, but rather adding the damned attachment.

There are many ways, but let's focus on how to do it with commonly installed programs first.

Throughout the examples, we'll assume that a function exists to determine the MIME time of the file given as argument. A simple way to implement it is to use the file utility with its --mime-type option.

get_mimetype(){
  # warning: assumes that the passed file exists
  file --mime-type "$1" | sed 's/.*: //' 
}

Also, where SMTP with authentication is used, we assume the variables $user, $password, $smtpserver and $smtpport are available and contain the obvious values.

Using sendmail

Here, "sendmail" just means "the executable named sendmail", and is not a reference to the MTA named "sendmail"; nowadays, an executable with this name is usually provided by any decent MTA, such as postfix or exim (or even nullmailer); of course, the sendmail MTA itself also provides such a binary.

In essence, an email with an attachment is normally implemented with a MIME message whose body is of type multipart/mixed: the first part is the message text, and the subsequent parts are the attachments (well, the order can be different, but it just seems logical to put the text in the first part). Each attachment can be encoded in various ways; here we're using the base64 encoding, since the utility to produce it is widely available.
Parts are separated by an arbitrary string, called "boundary". Ok, it's not arbitrary; RFC2046 has the syntax:

     boundary := 0*69<bchars> bcharsnospace

     bchars := bcharsnospace / " "

     bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" /
                      "+" / "_" / "," / "-" / "." /
                      "/" / ":" / "=" / "?"

But as long as it obeys the syntax above, for almost all practical purposes it's as if it's arbitrary (modulo the recommendations contained in the RFC about unpredictability and all that).

Here is some sample code to send the email using "sendmail":

#!/bin/bash

# some variables
# refactoring the script such that all these values are
# passed from the outside as arguments should be easy

from="sender@example.com"
to="recipient@example.org"
subject="Some fancy title"
boundary="ZZ_/afg6432dfgkl.94531q"
body="This is the body of our email"
declare -a attachments
attachments=( "foo.pdf" "bar.jpg" "archive.zip" )

# Build headers
{

printf '%s\n' "From: $from
To: $to
Subject: $subject
Mime-Version: 1.0
Content-Type: multipart/mixed; boundary=\"$boundary\"

--${boundary}
Content-Type: text/plain; charset=\"US-ASCII\"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

$body
"
 
# now loop over the attachments, guess the type
# and produce the corresponding part, encoded base64
for file in "${attachments[@]}"; do

  [ ! -f "$file" ] && echo "Warning: attachment $file not found, skipping" >&2 && continue

  mimetype=$(get_mimetype "$file") 
 
  printf '%s\n' "--${boundary}
Content-Type: $mimetype
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=\"$file\"
"
 
  base64 "$file"
  echo
done
 
# print last boundary with closing --
printf '%s\n' "--${boundary}--"
 
} | sendmail -t -oi   # one may also use -f here to set the envelope-from

Using "mail"

There are a few implementations of "mail" (or, more correctly these days, "mailx"), with slightly different syntax and capabilities.
The version that can do attachments is heirloom-mailx.

Since a sistem where mailx is installed should generally also have a "sendmail" binary, one may wonder why bother at all with "mail". Well, because it's simpler:

#!/bin/bash
 
# This needs heirloom-mailx
from="sender@example.com"
to="recipient@example.org"
subject="Some fancy title"
body="This is the body of our email"
declare -a attachments
attachments=( "foo.pdf" "bar.jpg" "archive.zip" )
 
declare -a attargs
for att in "${attachments[@]}"; do
  attargs+=( "-a"  "$att" )  
done
 
mail -s "$subject" -r "$from" "${attargs[@]}" "$to" <<< "$body"

Also we don't need to bother with MIME types because the program makes its own guesses based on its MIME databases.

This version of mailx can also send using SMTP (the man has all the details), so it does not necessarily need a local sendmail program:

# ... everything as before...
mail -s "$subject" -r "$from" -S smtp="smtp://${smtpserver}:${smtpport}" \
                              -S smtp-auth=login \
                              -S smtp-auth-user="$user" \
                              -S smtp-auth-password="$password" \
                              -S sendwait \
                              "${attargs[@]}" "$to" <<< "$body"
 
# use the correct value for smtp-auth, and also -S smtp-use-starttls to do TLS

As said, all the above needs heirloom-mailx; bsd-mailx cannot do (real) attachments.

Other tools

Here are some other tools that can send email with attachments, but are somewhat less commonly installed.

Mutt

Mutt can send emails both by invoking sendmail or by talking directly to a SMTP server.

# send using sendmail
$ mutt -e "set from=$from" -s "$subject" -a foo.pdf -a bar.jpg -- "$to" <<< "$body"   # also 'set use_envelope_from=yes' if needed
 
# send using smtp
$ mutt -e "set from=$from" \
       -e "set smtp_url=\"smtp://${user}@${smtpserver}:${smtpport}\"" \
       -e "set smtp_pass=\"$password\"" -s "$subject" -a foo.pdf -a bar.jpg -- "$to" <<< "$body"
 
# also 'set ssl_starttls=yes|no' to do/not do TLS

To simplify things, some of the settings above can be saved in mutt's configuration file to avoid specifying them on the command line.

sendEmail

Another one is sendEmail, which is available packaged for many distibutions and apparently can only send through SMTP (not sendmail):

$ sendEmail -f "$from" -t "$to" -m "$body" -u "$subject" -s "${smtpserver}:${smtpport}" -xu "$user" -xp "$password" -a foo.pdf -a bar.jpg
Swaks

Swaks is the self-defined "Swiss Army Knife for SMTP", so it can, not surprisingly, send emails.

Basic usage to send emails with attachments (via SMTP):

# if MIME type application/octet-stream is fine
$ swaks -s "${smtpserver}" -p "${smtpport}" -t "$to" -f "$from" --header "Subject: $subject" -S \
      --protocol ESMTP -a -au "$user" -ap "$password" --body "$body" \
      --attach foo.pdf  --attach bar.jpg
 
# to manually specifiy MIME types
$ swaks -s "${smtpserver}" -p "${smtpport}" -t "$to" -f "$from" --header "Subject: $subject" -S \
      --protocol ESMTP -a -au "$user" -ap "$password" --body "$body" \
      --attach-type "$(get_mimetype foo.pdf)" --attach foo.pdf \
      --attach-type "$(get_mimetype bar.jpg)" --attach bar.jpg
 
# yes, MIME type has to go before the file name.
# To do SSL/TLS, see the various --tls* options

Swaks is smart about guessing whether the option arguments it's given represent files or strings to take as-is (where applicable), wihch is good because the same sintax does it all. It can also use plain SMTP, which is fine if neither authentication nor SSL/TLS are needed.

Metasend

Metasend (from metamail) uses sendmail, needs body text in a file and explicit MIME types:

$ metasend -b -s "$subject" -S 100000000 -F "$from" -t "$to" -f "$bodyfile" -m text/plain -e 7bit \
                                                          -n -f foo.pdf -m "$(get_mimetype foo.pdf)" -e base64 \
                                                          -n -f bar.jpg -m "$(get_mimetype bar.jpg)" -e base64

After the textual body part, each new file to attach is introduced by the -n switch, and for each file three pieces of informations are given: the file path with -f, its MIME type with -m, and the encoding with -e.
By default metasend would split the message into multiple parts, because it assumes that the receiver uses metamail which is able to reassemble them; since this is not always the case, the -S option can be used to specify a message size under which no split will be performed. Ideally, this has to be larger than the sum of all the encoded message parts. In the example it's just some big number, but it can be calculated more accurately.

Mpack

mpack uses sendmail, only allows one attachment at a time and needs explicit MIME type from user. Furthermore, the body text must be in a file, and it's not possible to specify a sender address, so it seems to be the least powerful option:

$ mpack -s "$subject" -c "$(get_mimetype "foo.pdf")" -d "$bodyfile" foo.pdf "$to"

Conclusion

Some final remarks:

  • As usual when sending mail, even when specifying a From: address (or the envelope "mail from" where supported), local sendmail configuration or SMTP server configuration can override that information, so it's not 100% sure that those fields will not be touched.
  • When sending through SMTP, if authentication is not needed the relevant options can of course be omitted.
  • Even when the body text is expected to be in a file, if we have it in a variable the program can probably be fooled using process substitution shell trickery, like eg program ... -f <(echo "$body") ... or whatever the option is.

17 Comments

  1. Reddy Jahnavi Tenepalli says:

    I used your mpack command.I'm getting too many arguments error.Can you please help

  2. Amol Aranke says:

    I have created below script to attached a CSV File. The File is getting generated, but its truncating the header row of CSV incorrectly and also there is one more file thats getting attached with the email, namely 'ATT0001.txt' with every email. Anything wrong that you could found out here?

    SCRIPT
    --------------

    (
    echo "From:"$1;
    echo "To:"$2;
    echo "Subject:"$3;
    echo "MIME-Version: 1.0";
    echo "Content-Type:multipart/mixed; boundary=\"B835649000072104Jul07\"";

    echo "--B835649000072104Jul07";
    echo "Content-Type: text/html; charset=\"UTF-8\"";
    echo "Content-Transfer-Encoding: 7bit";
    echo "Content-Disposition: inline";
    echo "";
    echo "$4";

    echo "--B835649000072104Jul07";
    echo "Content-Type: text/csv";
    echo "Content-Transfer-Encoding: base64";
    echo "Content-Disposition: attachment; filename=\"$5\"";
    base64 "$5"

    echo "--B835649000072104Jul07";
    ) | sendmail -t

    --------------------

    • waldner says:

      The last boundary marker should end with -- as well, eg

      ...
      base64 "$5"
      
      echo "--B835649000072104Jul07--";
      ) | sendmail -t
      

      Don't know whether that is the problem in your case, but it's something that definitely needs fixing.

  3. Miguel says:

    Hi, very good article!!

    I'm using your "sendmail" section to create mails, but I have a problem with PDF docs attaching. In fact, it's the base64 instruction that's not recognized by my Unix server.

    Could you please help me on how to proceed, or if there is any other workaround method?

    Best regards

    Miguel

    • waldner says:

      What does it mean that base64 is "not recognized"? If it's not installed, you'll have to install the package containing the program using your distribution's package manager. However it should be in the "coreutils" package (at least in recent distributions), which is almost surely guaranteed to be installed on any system. Perhaps your PATH does not include its location? Without more details on the error it's impossible to tell.

  4. Christiaan says:

    Hi Waldner,

    2017 and still a great article!

    I used this to generate HTML emails with logo for an Icinga 2 monitoring solution.

    I used sendEmail on Ubuntu 16.04 but wanted to point out that absolute paths are required for the -a parameters. I was not receiving emails and tested through. I would receive emails until I added -a foo.jpg.

    Not working:
    sendEmail -f "$from" -t "$to" -m "$body" -u "$subject" -s 127.0.0.1 -a logo.jpg

    Working
    sendEmail -f "$from" -t "$to" -m "$body" -u "$subject" -s 127.0.0.1 -a /some/path/to/file/lgo_sage.png

    Hope that helps someone else

  5. Netaji says:

    Thanks, a well neatly presented article.

  6. Thanks for that. It was really helpfull for me.

    But I am seeing a problem I was not able to solve myself.

    I'm using your sendmail example to send attachments. The files to be attached are on a different directory than the actual .sh script and those files are being named like this:

    The file full path is: /some/folder/attachments/File.txt
    And the file is being sent with this name: somefolderattachmentsFile.txt and I would like it to be named as File.txt.

    Could you please explain how can I do that using your sendmail script?

    • waldner says:

      Well, you would have to extract the base name of the file, so for example you could replace the following line:

      Content-Disposition: attachment; filename=\"$file\"
      

      with this

      Content-Disposition: attachment; filename=\"$(basename "$file")\"
      
  7. Pyero says:

    Hi Waldner,
    thank you for summarizing the different ways to send mail for each tool.
    It's really nice to have all the information in one place :)

    Just a side note: you do need to use a for loop, or a 2nd array.
    Example:
    $ attachments=( "foo.pdf" "bar.jpg" "archive.zip" )
    $ attachments=( ${attachments[@]/#/-a } )
    $ echo ${attachments[@]}
    -a foo.pdf -a bar.jpg -a archive.zip

    The 2nd line will prepend "-a " (mind the space!) to the name of the files. ;)
    Thanks.

    • waldner says:

      (I suppose you mean "you DON'T need to use a for loop)

      Your solution works assuming the filenames don't contain spaces. If that's not the case, you have trouble:

      $ attachments=( "foo.pdf" "bar.jpg" "arch ive.zip" )
      $ attachments=( ${attachments[@]/#/-a } )
      $ declare -p attachments
      declare -a attachments='([0]="-a" [1]="foo.pdf" [2]="-a" [3]="bar.jpg" [4]="-a" [5]="arch" [6]="ive.zip")'
      

      Alright, so you can use quotes:

      $ attachments=( "foo.pdf" "bar.jpg" "arch ive.zip" )
      $ attachments=( "${attachments[@]/#/-a }" )
      

      But then:

      $ declare -p attachments
      declare -a attachments='([0]="-a foo.pdf" [1]="-a bar.jpg" [2]="-a arch ive.zip")'
      

      Which again is troublesome if you want (as is the case) the program to see each "-a" and the associated filenames as separate arguments.

  8. thejeswi says:

    Thanks Waldner. It worked!!!

  9. Thejeswi says:

    I tried your example for sendmail with multiple attachments. It attaches the documents properly however the email doesn't contain the body. I am not sure why.

    My actual requirement is to create a html body with different attachment types. I am unable to achieve this.

    Either I get a HTML body or a PDF attachment but not both together.

    • waldner says:

      Indeed, I too don't seem to be able to get the body, only the attachment. I'm pretty sure it used to work though, so I'm checking what's going on.

    • waldner says:

      So it seems there were formatting problems caused by the syntax highlighter. You shuld copy/paste the script again.
      That said, you should be able to send an HTML email with attachments. If you want pure HTML, they you just replace the text/plain part with something like

      --${boundary}
      Content-Type: text/html; charset=\"UTF-8\"
      Content-Transfer-Encoding: quoted-printable
      
      some (possibly encoded) HTML content here...
      
      --${boundary}
      
      ... attachments here ...
      
      --${boundary}--
      

      of course using whatever charset and encoding is suitable for your scenario.
      If you want a multipart/alternative part, so the receiver can choose whether to show the text or the HTML version, you do something like

      --${boundary}
      Content-Type: multipart/alternative; boundary=this_needs_its_own_boundary_zzzz
      
      --this_needs_its_own_boundary_zzzz
      Content-Type: text/plain; charset=US-ASCII
      Content-Transfer-Encoding: 7bit
      
      Some text content here...
      
      --this_needs_its_own_boundary_zzzz
      Content-Type: text/html; charset=UTF-8
      Content-Transfer-Encoding: quoted-printable
      
      Some (possibly encoded) HTML content here...
      
      --this_needs_its_own_boundary_zzzz--
      
      --${boundary}
      
      ...attachments here ...
      
      --${boundary}--
      

      Again, adapt as needed.