Skywalker13

Diary of an ex-GeeXboX developer…

IMAP library with Shell scripting

Posted at — Dec 18, 2013

This day I will share with you how is possible to create a library in shell for the IMAP protocol. The idea is to find a mail in a mailbox only with a shell like BASH and the very powerful command nc.

What is nc? This is the netcat command; the TCP/IP swiss army knife. Here you will find a use case in order to use netcat as IMAP client.

You can create an empty shell script, something like imap.sh. At the top, you can add some lines like:

#!/bin/bash

[ ! -f ~/.gala_config ] && echo "file .gala_config not found" && exit 1

. ~/.gala_config

Why gala_? It is just a namespace, you can use an other word. It doesn’t matter. In the .gala_config file you should put the settings for your email account and the server. For example:

mail_server=imap.mymailserver.com
mail_port=143
mail_user=myemail@mymailserver.com
mail_passwd=mypassword

Now we can implement the login function in order to open an IMAP session on the mail server.

function gala_imap_login()
{
  local user passwd

  user=$1
  passwd=$2
  [ -z "$user" ] && user=$mail_user
  [ -z "$passwd" ] && passwd=$mail_passwd

  rm -f ./.ncin ./.ncout
  mkfifo ./.ncin ./.ncout
  exec 5<>./.ncin 6<>./.ncout

  nc -w 20 $mail_server $mail_port <&5 >&6 &

  gala_imap_send "login" "$user" "$passwd"
  [ "$?" != 0 ] && return 1 || return 0
}

The idea is to run netcat in background. Two FIFOs will be used for the stdin and stdout of netcat, and exec is very useful in order to bind the fifos to file descriptors. Note that I use the descriptors 5 and 6 for that. The -w option with netcat is just the timeout in seconds.

Emi (see comments) has provided a version for TLS connection with OpenSSL. In this case, you should change the netcat nc command line by:

openssl s_client -quiet -crlf -connect $mail_server:$mail_port <&5 >&6 &

Now that netcat is running, we can write the function able to send the commands (IMAP protocol).

function gala_imap_send()
{
  local result line

  OUTPUT=""

  echo "A0 $@" >&5

  while read -t 20 result; do
    line="`echo "$result" | tr -d '\r'`"
    OUTPUT="$OUTPUT
$line"
    echo "$line" | grep "^A0 OK" >/dev/null && return 0
    echo "$line" | grep -E "^A0 BAD|^A0 NO" >/dev/null && echo "imap:error:$line" && return 1
  done <&6

  return 1
}

It is very simple, we send the command with echo, and the results are written in the OUTPUT variable. All commands are sent with the string “A0 " at the beginning. But you can use an other string. This one is just used in order to identify the response associated to the command. Imagine that you send 2 commands successively. You should use two different prefixes. Then you can identify the response for each command. With echo, we use the file descriptor 5 which was associated to the stdin fifo in the login function.

For the response, we read the fifo on the file descriptor 6; with a timeout of 20 seconds. OUTPUT is the whole result, then we can check if the command was done with success or not (OK or BAD).

Well, how was the login?

gala_imap_send "login" "$user" "$passwd"

The protocol is just like that:

"A0 login mylogin mypasswd\n"

And for the logout, it is very easy:

function gala_imap_logout()
{
  gala_imap_send "logout"

  rm -f ./.ncin ./.ncout
  return 0
}

OK, but before the logout, we should do something with the mailbox. The first thing will be to select the mailbox to use.

function gala_imap_select()
{
  local mbox

  mbox=$1
  [ -z "$mbox" ] && mbox=INBOX

  gala_imap_send "select" "$mbox"
  [ "$?" != 0 ] && return 1 || return 0
}

With this new function, it will select the INBOX mailbox by default.

and how to search for a mail?

function gala_imap_search()
{
  RESULT=""
  gala_imap_send "search" $@
  [ "$?" != 0 ] && return 1

  RESULT=`echo "$OUTPUT" | grep "^* SEARCH" | sed 's,.*SEARCH \(.*[0-9]*\).*,\1,'`
  return 0
}

When you search something, the result is always a list of id. The id can be used in order to retrieve more details on the mail in a second step. You can search with many ways. Here an example:

gala_imap_search "subject" "foobar" "from" "@schroetersa.ch" "undeleted"

It will retrieve all undeleted emails in the INBOX with the word “foobar” in the subject and sent by someone at @schroetersa.ch. Look the IMAP RFC for more details (URL at the end).

When you have at least one id, you can use it to find a field in the header.

function gala_imap_fetch_header()
{
  local uid tag

  uid=$1
  tag=$2

  RESULT=""
  gala_imap_send "fetch" "$uid" "body[header.fields ($tag)]"
  [ "$?" != 0 ] && return 1

  RESULT=`echo "$OUTPUT" | grep -i "^$tag:" | sed "s,.*: \(.*\)[[:space:]]*,\1,"`
  return 0
}

We can use this new function in order to find the whole subject.

gala_imap_fetch_header "$uid" subject

An other example, how to delete a mail?

function gala_imap_delete()
{
  local uid

  uid=$1

  gala_imap_send "store" "$uid" "flags" "\\Deleted"
  [ "$?" != 0 ] && return 1 || return 0
}

And finally, an example which uses this library:

#!/bin/bash
. `dirname $0`/lib/imap.sh

gala_imap_login
[ "$?" != 0 ] \
  && printf "imap_error:login\n$OUTPUT\n" \
  && gala_imap_logout \
  && exit 1

gala_imap_select
[ "$?" != 0 ] \
  && printf "imap_error:select\n$OUTPUT\n" \
  && gala_imap_logout \
  && exit 1

gala_imap_search "subject" "foobar" "from" "@schroetersa.ch" "undeleted"
[ "$?" != 0 ] \
  && printf "imap_error:search\n$OUTPUT\n" \
  && gala_imap_logout \
  && exit 1

arruid=($RESULT)
arrlen=${#arruid[@]}
# nothing to do ?
[ "$arrlen" = 0 ] \
  && echo "nothing" \
  && gala_imap_logout \
  && exit 0

last=$((arrlen - 1))
uid=${arruid[$last]}

gala_imap_fetch_header "$uid" "subject"
[ "$?" != 0 ] \
  && printf "imap_error:fetch\n$OUTPUT\n" \
  && gala_imap_logout \
  && exit 1

echo "$RESULT"

gala_imap_delete "$uid"
gala_imap_logout

This example looks for the mails “foobar” from @schroetersa.ch, prints the whole subject of one mail and deletes this mail.

Easy, isn’t it? Feel free to improve these scripts. Look for the RFC 2060 because you can do many things with the IMAP protocol. I put here just some examples.

Remember that the shell is your friend. Everything are possible only with the common commands. These examples are usable on all UNIX OS.