Next: , Previous: , Up: sendmail   [Contents][Index]


A.3 Send email through a process in background

The script below can be found in examples/sendmail-connector.sh. It just sends a hard–coded email message, from a hard–coded address to a hard–coded address. It makes no use of MBFL.

Bash version 4 introduced the new keyword coproc, which can be used to spawn processes in background and talk to them via pipes. This keyword in not used in this appendix.

The purpose of the script is to understand how to send a message through a process in background. It does the same things of the example described in Just send an email message. The main difference is that the single function open_session() is replaced by the two functions open_session() and connector().

What is important to understand, is how open_session() runs connector() in background and sets up two file descriptors to talk to it. In the real world we never use this technique with a function; this example script makes use of connector() as a replacement for an external program that can establish sophisticated connections to remote hosts, for example using the TLS/SSL protocols.

Function: open_session hostname

Open a connection to the SMTP server spawning a background process represented by the connector() function. It makes use of two FIFOs (First In, First Out).

If we were to do it from a C language program: we would use the pipe() system function to create two pipes connecting script’s process to the background process.

 ---------  out pipe  -----------  socket  --------
| script  |--------->| connector |<======>| SMTP   |
| process |<---------| process   |        | server |
 ---------  in pipe   -----------          --------

Bash has no way to create a pipe using the pipe() system function (up until version 4), so we use two FIFO channels created by the mkfifo program:

: ${TMPDIR:=/tmp}
local INFIFO=${TMPDIR}/in.$$
local OUFIFO=${TMPDIR}/out.$$

mkfifo --mode=0600 $INFIFO $OUFIFO

the script will use INFIFO to read characters from connector(), and OUFIFO to send characters to connector().

 ---------  OUFIFO   -----------  socket  --------
| script  |-------->| connector |<======>| SMTP   |
| process |<--------| process   |        | server |
 ---------  INFIFO   -----------          --------

Once the FIFOs exist on the file system, we run connector() in background, connecting its standard input and output to the FIFOs:

connector $HOSTNAME $SMTP_PORT <$OUFIFO >$INFIFO &

be careful in selecting the redirections. Notice that, in this simple example, we ignore errors running connector().

Now we open file descriptors connecting them to the FIFOs:

exec 3<>$INFIFO 4>$OUFIFO

the script will use file descriptor 3 to read characters from connector(), and file descriptor 4 to send characters to connector(). We open the input FIFO for both reading and writing, else exec will block waiting for the first char.

We have connected both the ends of both the FIFOs, so we can remove them from the file system:

rm $INFIFO $OUFIFO

the FIFOs will continue to exist in the OS kernel until the file descriptors are closed.

Finally we register a clean up handler that closes the descriptors:

trap 'exec 3<&- 4>&-' EXIT
Function: connector hostname

Establish a connection to the SMTP server at hostname. It is not important here to fully understand how this function works; suffice it to say that it reads lines from stdin, and echoes them to the server; it reads lines from the server, and echoes them to stdout.

#! /bin/bash
#
# Part of: Marco's Bash Functions Library
# Contents: example script to send email using bg process
# Date: Thu Apr 23, 2009
#
# Abstract
#
#       This  script  just  sends  a  hardcoded  email  message  from  a
#       hardcoded address  to a hardcoded  address.  It makes no  use of
#       MBFL.
#
#         The purpose  of this  script is  to understand  how to  send a
#       message through a process in background.
#
# Copyright (c) 2009, 2010, 2018 Marco Maggi <marco.maggi-ipsu@poste.it>
#
# The author hereby grants  permission to use, copy, modify, distribute,
# and  license this  software  and its  documentation  for any  purpose,
# provided that  existing copyright notices  are retained in  all copies
# and that  this notice  is included verbatim in any  distributions.  No
# written agreement, license, or royalty  fee is required for any of the
# authorized uses.  Modifications to this software may be copyrighted by
# their authors and need not  follow the licensing terms described here,
# provided that the new terms are clearly indicated on the first page of
# each file where they apply.
#
# IN NO  EVENT SHALL THE AUTHOR  OR DISTRIBUTORS BE LIABLE  TO ANY PARTY
# FOR  DIRECT, INDIRECT, SPECIAL,  INCIDENTAL, OR  CONSEQUENTIAL DAMAGES
# ARISING OUT  OF THE  USE OF THIS  SOFTWARE, ITS DOCUMENTATION,  OR ANY
# DERIVATIVES  THEREOF, EVEN  IF THE  AUTHOR  HAVE BEEN  ADVISED OF  THE
# POSSIBILITY OF SUCH DAMAGE.
#
# THE  AUTHOR  AND DISTRIBUTORS  SPECIFICALLY  DISCLAIM ANY  WARRANTIES,
# INCLUDING,   BUT   NOT  LIMITED   TO,   THE   IMPLIED  WARRANTIES   OF
# MERCHANTABILITY,    FITNESS   FOR    A    PARTICULAR   PURPOSE,    AND
# NON-INFRINGEMENT.  THIS  SOFTWARE IS PROVIDED  ON AN "AS  IS" BASIS,
# AND  THE  AUTHOR  AND  DISTRIBUTORS  HAVE  NO  OBLIGATION  TO  PROVIDE
# MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#

PROGNAME=${0##*/}
: ${TMPDIR:=/tmp}

function main () {
    local HOSTNAME=localhost
    local SMTP_PORT=25
    local FROM_ADDRESS=marco@localhost
    local TO_ADDRESS=root@localhost
    local LOGGING_TO_STDERR=yes

    open_session "$HOSTNAME"
    recv 220
    send 'HELO %s' 127.0.0.1
    recv 250
    send 'MAIL FROM:<%s>' "$FROM_ADDRESS"
    recv 250
    send 'RCPT TO:<%s>' "$TO_ADDRESS"
    recv 250
    send %s DATA
    recv 354
    print_message | read_and_send_message
    send %s .
    recv 250
    send %s QUIT
    recv 221
}
function print_message () {
    local LOCAL_HOSTNAME DATE MESSAGE_ID MESSAGE
    LOCAL_HOSTNAME=$(hostname --fqdn) || exit 2
    DATE=$(date --rfc-2822) || exit 2
    printf -v MESSAGE_ID '%d-%d-%d@%s' \
        $RANDOM $RANDOM $RANDOM "$LOCAL_HOSTNAME"
    MESSAGE="Sender: $FROM_ADDRESS
From: $FROM_ADDRESS
To: $TO_ADDRESS
Subject: demo from $PROGNAME
Message-ID: <$MESSAGE_ID>
Date: $DATE

This is a text demo from the $PROGNAME script.
--\x20
Marco
"
    printf "$MESSAGE"
}
function open_session () {
    local HOSTNAME=${1:?}
    local INFIFO=${TMPDIR}/in.$$
    local OUFIFO=${TMPDIR}/out.$$
    # Bash  has  no  operation  equivalent to  the  C  level
    # "pipe()" function, so we have to use FIFOs.
    mkfifo --mode=0600 $INFIFO $OUFIFO
    connector "$HOSTNAME" <$OUFIFO >$INFIFO &
    # Open the input FIFO for both reading and writing, else
    # "exec" will block waiting for the first char.
    exec 3<>$INFIFO 4>$OUFIFO
    # We have connected both the  ends of both the FIFOs, so
    # we  can remove them  from the  file system:  the FIFOs
    # will continue to exist  until the file descriptors are
    # closed.
    rm $INFIFO $OUFIFO
    trap 'exec 3<&- 4>&-' EXIT
}
function recv () {
    local EXPECTED_CODE=${1:?}
    local line=
    IFS= read line <&3
    if test "$LOGGING_TO_STDERR" = yes
    then printf '%s log: recv: %s\n' "$PROGNAME" "$line"
    fi
    if test "${line:0:3}" != "$EXPECTED_CODE"
    then
        send %s QUIT
        # It is cleaner to wait for the reply from the
        # server.
        IFS= read line <&3
        if test "$LOGGING_TO_STDERR" = yes
        then printf '%s log: recv: %s\n' "$PROGNAME" "$line"
        fi
        exit 2
    fi
}
function send () {
    local template=${1:?}
    shift
    local line
    printf -v line "$template" "$@"
    printf '%s\r\n' "$line" >&4
    if test "$LOGGING_TO_STDERR" = yes
    then printf '%s log: sent: %s\n' "$PROGNAME" "$line"
    fi
}
function read_and_send_message () {
    local line
    local -i count=0
    while IFS= read line
    do
        if test "${line:0:1}" = '.'
        then printf '.%s\r\n' "$line" >&4
        else printf  '%s\r\n' "$line" >&4
        fi
        let ++count
    done
    if test "$LOGGING_TO_STDERR" = yes
    then printf '%s log: sent message (%d lines)\n' "$PROGNAME" $count
    fi
}
function connector () {
    local HOSTNAME=${1:?} query= answer= line=
    local DEVICE
    printf -v DEVICE '/dev/tcp/%s/%d' "$HOSTNAME" $SMTP_PORT
    exec 3<>"$DEVICE"
    # Read the  greetings from the server, echo  them to the
    # client.
    IFS= read -t 5 answer <&3
    if ((127 < $?))
    then
        printf '%s: connection timed out\n' "$PROGNAME" >&2
        exit 2
    fi
    printf '%s\n' "$answer"
    # Read the query from the client, echo it to the server.
    while read query
    do
        printf '%s\r\n' "$query" >&3
        # Read the  answer from the  server, echo it  to the
        # client.
        IFS= read -t 5 answer <&3
        if ((127 < $?))
        then
            printf '%s: connection timed out\n' "$PROGNAME" >&2
            exit 2
        fi
        printf '%s\n' "$answer"
        # Test special queries.
        if test "$query" = QUIT$'\r'
        then
            IFS= read -t 5 answer <&3
            if ((127 < $?))
            then
                printf '%s: connection timed out\n' "$PROGNAME" >&2
                exit 2
            fi
            printf '%s\n' "$answer"
            exit
        fi
        if test "$query" = DATA$'\r'
        then
            # Read data lines from  the client, echo them to
            # the server up until ".\r" is read.
            while IFS= read line
            do
                printf '%s\n' "$line" >&3
                test "${line:0:2}" = .$'\r' && break
            done
            # Read the answer to  data from the server, echo
            # it to the client.
            IFS= read -t 5 answer <&3
            if ((127 < $?))
            then
                printf '%s: connection timed out\n' "$PROGNAME" >&2
                exit 2
            fi
            printf '%s\n' "$answer"
        fi
    done
    # We should never come here.
    exit 1
}

main

### end of file

Next: , Previous: , Up: sendmail   [Contents][Index]

This document describes version 3.0.0-devel.0 of Marcos Bash Functions Library.