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.
I used your mpack command.I'm getting too many arguments error.Can you please help
Without seeing what you actually typed it's impossible to help.
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
--------------------
The last boundary marker should end with -- as well, eg
Don't know whether that is the problem in your case, but it's something that definitely needs fixing.
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
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.
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
Interesting. Thanks for sharing!
Thanks, a well neatly presented article.
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?
Well, you would have to extract the base name of the file, so for example you could replace the following line:
with this
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.
(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:
Alright, so you can use quotes:
But then:
Which again is troublesome if you want (as is the case) the program to see each "-a" and the associated filenames as separate arguments.
Thanks Waldner. It worked!!!
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.
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.
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
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
Again, adapt as needed.