bash で任意の SMTP サーバへメールを送信する

呼称: Mailmail
目的: コマンドラインで使える「ちょっとした」メール送信クライアントを作成する
特徴: 任意のファイルを添付できる(たぶん)、bash で動く
用例: テストスクリプトに組み込んで、実行結果のログをメールに添付して送信する
備考: bash のソケット機能を使用している、SMTP 認証は未対応

hi_saito さんが gawk でメール送信クライアントを作成されていました。(最下部リファレンスを参照) それに対抗(?)して、bash で作ったメール送信クライアントも晒し、、、いや、公開してみます。bash しか使えないような、やや特殊な環境で役に立つかも?(^ ^;;

#!/bin/bash

###########################################################
# GLOBAL (default value)
###########################################################
PROG_NAME=$(basename $0)
MAIL_SERVER="localhost"
SMTP_PORT=25
FROM="${USER}@${HOSTNAME}"
RCPT_TO=()
HEADER_TO=
SUBJECT="This mail is sent by ${PROG_NAME}"
BODY=$(mktemp /tmp/${PROG_NAME}_body.XXXXXXXXXXXX)
ATTACH_FILE=
DATE=$(date +%Y%m%d%H%M%S)
LOG_FILE="/tmp/$(echo ${PROG_NAME} | sed "s/\.sh//").log"
FD_NUM=5
DEBUG_MODE="off"

# for error check
LAST_FUNCNAME=
NORMAL_END=0
UNKNOWN_OPTION=1
MISSING_ADDRESS=2
NOT_NUMERIC=3
INVALID_PORT=4
INVALID_ATTACH=5
UNKNOWN_SMTP_250REPLY=6
UNKNOWN_SMTP_ERROR=7

###########################################################
# Trap 
###########################################################
trap do_exit 0 1 2 3 13 15

###########################################################
# Run Before exit, post-processing
###########################################################
do_exit() {
    
    [ -f ${BODY} ] && rm -f ${BODY}
    if [ -S /dev/fd/${FD_NUM} ]; then
        log "*** Closing socket."
        eval "exec ${FD_NUM}<&-"
    fi
}

###########################################################
# Main
###########################################################
do_main() {

    rm -f ${LOG_FILE}
    log "* Starting ${PROG_NAME} ${FROM} on ${DATE}"
    get_args "$@" || return $?
    send_mail || return $?
    
    return ${NORMAL_END}
}

###########################################################
# Usage
###########################################################
usage() {
    
    echo -n "usage: bash-socket_mail.sh -t"
    echo -n " <to_address1 to_address2 ...>"
    echo -n " [ -m <mail_server> ] [ -p <smtp_port> ]"
    echo -n " [ -f <from_address> ] [ -s <subject> ]"
    echo -n " [ -b <mail_body> ]"
    echo    " [ -a <attach_files1 attach_file2 ...> ] [-v]"
    echo -n "     : '-v' option is used for DEBUG"
    echo    "(just some log is displayed)"
    echo ""
}

###########################################################
# Output to logfile
###########################################################
log() {
    
    if [ ${DEBUG_MODE} = "off" ]; then
        echo "$*" >> ${LOG_FILE}
    else
        echo "$*" | tee -a ${LOG_FILE}
    fi
}

###########################################################
# Parse script option/argument
###########################################################
get_args() {

    LAST_FUNCNAME=${FUNCNAME}
    
    local opt= addr= afile=
    local pflg="off" tflg="off" aflg="off" 
   
    while getopts m:p:f:t:s:b:a:v opt
    do
        case ${opt} in
        m)
            MAIL_SERVER=${OPTARG}
            ;;
        p)
            SMTP_PORT=${OPTARG}
            pflg="on"
            ;;
        f)
            FROM=${OPTARG}
            ;;
        t)
            RCPT_TO=($(echo ${OPTARG} | sed "s/,*\s\s*,*/ /g"))
            HEADER_TO=$(echo ${RCPT_TO[@]} | sed "s/\s/, /g")
            tflg="on"
            ;;
        s)
            SUBJECT=${OPTARG}
            ;;
        b)
            echo -en "${OPTARG}" > ${BODY}
            ;;
        a)
            ATTACH_FILE=${OPTARG}
            aflg="on"
            ;;
        v)
            DEBUG_MODE="on"
            ;;
        *)
            return ${UNKNOWN_OPTION}
            ;;
        esac
    done

    # destination address check
    [ ${tflg} = "off" ] && return ${MISSING_ADDRESS}

    # port number check
    if [ ${pflg} = "on" ]; then
        [ -z $(echo ${SMTP_PORT} | egrep "^[0-9]+$") ] && \
        return ${NOT_NUMERIC}
        [ ${SMTP_PORT} -ne 25 ] && \
        [ ${SMTP_PORT} -lt 1024 -o ${SMTP_PORT} -gt 65535 ] && \
        return ${INVALID_PORT}
    fi
    
    # attach file check
    if [ ${aflg} = "on" ]; then
        for afile in ${ATTACH_FILE}
        do
            [ ! -f ${afile} ] && return ${INVALID_ATTACH}
        done
    fi
    
    return ${NORMAL_END}
}

###########################################################
# check all error pattern
###########################################################
check_error() {

    local result=$1

    if [ ${result} != ${NORMAL_END} ]; then
        log "*** Error function : ${LAST_FUNCNAME}"
    fi
    
    case ${result} in
        ${NORMAL_END})
            log "*** ${PROG_NAME} is completed."
            ;;
        ${UNKNOWN_OPTION})
            log "unknown_option."
            ;;
        ${MISSING_ADDRESS})
            log "missing destination mail address."
            ;;
        ${NOT_NUMERIC})
            log "port number is not numeric."
            ;;
        ${INVALID_PORT})
            log "port number should be between 1024 and 655535."
            ;;
        ${INVALID_ATTACH})
            log "cannot find attach file."
            ;;
        ${UNKNOWN_SMTP_250REPLY})
            log "unkown pattern getting 250 reply via SMTP."
            ;;
        ${UNKNOWN_SMTP_ERROR})
            log "unkown error on SMTP connection."
            ;;
        *)
            log "unknown error."
            ;;
    esac

    [ ${result} = ${UNKNOWN_OPTION} ] || 
    [ ${result} = ${MISSING_ADDRESS} ] || 
    [ ${result} = ${INVALID_ATTACH} ] && usage

    return ${result}
}

###########################################################
# send mail using bash socket
###########################################################
send_mail() {

    LAST_FUNCNAME=${FUNCNAME}
    
    local i=0 j=0
    local line=
    local d_num=$((${#RCPT_TO[@]} + 2)) 
    local q_num=$((${d_num} + 1))
    
    # open socket to mail server
    eval "exec ${FD_NUM}<>/dev/tcp/${MAIL_SERVER}/${SMTP_PORT}"

    while read line; do
        # logging response from mail server
        log ${line}
        set - ${line}
        case $1 in
        220) # <domain> Service ready
            log "*** Connected to ${MAIL_SERVER}:${SMTP_PORT}."
            echo "EHLO localhost" >&${FD_NUM}
            ;;
        250) # Requested mail action okay, completed
            let i++
            log "*** get OK reply from mail server."
            if [ ${i} = 1 ]; then
                log "*** Sending recipient address."
                log "    - mail from : ${FROM}"
                echo -en "mail from: ${FROM}\r\n" >&${FD_NUM}
            elif [ ${i} -ge 2 -a ${i} -lt ${d_num} ]; then
                log "*** Sending destination address."
                log "    - rcpt to : ${RCPT_TO[${j}]}"
                echo -en "rcpt to: ${RCPT_TO[${j}]}\r\n" >&${FD_NUM}
                let j++
            elif [ ${i} = ${d_num} ]; then # data starting... now!
                log "*** Starting data transfer."
                echo -en "data\r\n" >&${FD_NUM}
            elif [ ${i} = ${q_num} ]; then # data successfully received
                log "*** Sending quit."
                echo -en "quit\r\n" >&${FD_NUM}
            else # we don't expect more than 3 250-OK responses.
                log "*** Sending rset and quit."
                echo -en ".\r\nrset\r\nquit\r\n" >&${FD_NUM}
                return ${UNKNOWN_SMTP_250REPLY}
            fi
            ;;
        354) # Start mail input; end with <CRLF>.<CRLF>
            if [ -z "${ATTACH_FILE}" ]; then
                make_mail_data
            else
                make_mail_data_with_attach
            fi
            ;;
        [0-9]*-*) # followup lines, don't bother
            true
            ;;
        221) # <domain> Service closing transmission channel
            log "*** Closing transmission channe."
            true
            ;;
        *) # whoops, something happened .
            log "*** Error sending mail"
            echo -en ".\r\nrset\r\nquit\r\n" >&${FD_NUM}
            return ${UNKNOWN_SMTP_ERROR}
            ;;
        esac
    done <&${FD_NUM}
    
    return ${NORMAL_END}
}

###########################################################
# send simple mail data
###########################################################
make_mail_data() {

    LAST_FUNCNAME=${FUNCNAME}

    local header=
    local data_end=".\r\n"
    local ctype="$(file -ib ${BODY})"
    local cencoding="$(get_encoding_bit "${ctype}")"
    
    # make simple mail header
    header="From: ${FROM}\r\nTo: ${HEADER_TO}\r\n"
    header="${header}Subject: ${SUBJECT}\r\nContent-Type: ${ctype}\r\n"
    header="${header}Content-Transfer-Encoding: ${cencoding}\r\n\r\n"
    
    log "*** Sending mail header."
    echo -en "${header}" >&${FD_NUM}
    
    log "*** Sending mail body."
    cat ${BODY} >&${FD_NUM}
    echo -en "\r\n" >&${FD_NUM}
    echo -en "${data_end}" >&${FD_NUM}
    
    return ${NORMAL_END}
}

###########################################################
# send attached mail data
###########################################################
make_mail_data_with_attach() {

    LAST_FUNCNAME=${FUNCNAME}

    local header= f= fname=
    local data_end=".\r\n"
    local boundary="------------${DATE}.$(head -c10 /dev/urandom | md5sum | head -c15)"
    local ctype="multipart/mixed;\r boundary=\"${boundary}\""
    local mime_ver="1.0"
    local mime_ctype=
    local mime_disposition=
    local mime_encoding="base64"
    local mime_comment="This is a multi-part message in MIME format.\r\n"
    local body_ctype="$(file -ib ${BODY})"
    local body_cencoding="$(get_encoding_bit "${body_ctype}")"
    local body_header=
    
    # make MIME mail header
    header="From: ${FROM}\r\nTo: ${HEADER_TO}\r\nSubject: ${SUBJECT}\r\n"
    header="${header}MIME-Version: ${mime_ver}\r\nContent-Type: ${ctype}\r\n\r\n"
    # make body boundary
    body_header="--${boundary}\r\nContent-Type: ${body_ctype}\r\n"
    body_header="${body_header}Content-Transfer-Encoding: ${body_cencoding}\r\n\r\n"
    
    log "*** Sending mail header."
    echo -en "${header}" >&${FD_NUM}
    echo -en "${mime_comment}" >&${FD_NUM}
    echo -en "${body_header}" >&${FD_NUM}
    
    log "*** Sending mail body."
    cat ${BODY} >&${FD_NUM}
    echo -en "\r\n\r\n" >&${FD_NUM}
    
    log "*** Sending attach file."
    for f in ${ATTACH_FILE}
    do
        fname=$(basename ${f})
        mime_ctype="$(file -ib ${f})\r name=\"${fname}\""
        mime_disposition="inline;\r filename=\"${fname}\""
        log "    - file : ${f}"
        echo -en "--${boundary}\r\nContent-Type: ${mime_ctype}\r\n" >&${FD_NUM}
        echo -en "Content-Transfer-Encoding: ${mime_encoding}\r\n" >&${FD_NUM}
        echo -en "Content-Disposition: ${mime_disposition}\r\n\r\n" >&${FD_NUM}
        base64 ${f} >&${FD_NUM}
        echo -en "\r\n" >&${FD_NUM}
    done
    
    echo -en "--${boundary}\r\n" >&${FD_NUM}
    echo -en "${data_end}" >&${FD_NUM}
    
    return ${NORMAL_END}
}

###########################################################
# get encode 7bit or 8bit(simple check)
###########################################################
get_encoding_bit() {
    
    echo "$1" | grep "charset=us-ascii" > /dev/null 2>&1
    [ $? -eq 0 ] && echo "7bit" || echo "8bit"
}

###########################################################
# Run Main
###########################################################
do_main "$@" || check_error $?
exit $?

実行結果。

$ sh mailmail.sh -m localhost -p 25 -t xxx@gmail.com \
                 -f yyy@zzz -s send_test -b body_test \
                 -a ttt.txt -v
 220 localhost ESMTP Postfix             
 *** Connected to localhost:25.                                
 250-localhost
 250-PIPELINING                                                 
 250-SIZE 10240000                                        
 250-VRFY                                                 
 250-ETRN                                                 
 250-AUTH PLAIN LOGIN                                  
 250-ENHANCEDSTATUSCODES                                                 
 250-8BITMIME                                                 
 250 DSN                                             
 *** get OK reply from mail server.                      
 *** Sending recipient address.                          
    - mail from : yyy@zzz
 250 2.1.0 Ok                                       
 *** get OK reply from mail server.                      
 *** Sending destination address.
    - rcpt to : xxx@gmail.com
 250 2.1.5 Ok
 *** get OK reply from mail server.
 *** Starting data transfer.
 354 End data with .
 *** Sending mail header.
 *** Sending mail body.
 *** Sending attach file.
    - file : ttt.txt
 250 2.0.0 Ok: queued as 16E8552E4F5
 *** get OK reply from mail server.
 *** Sending quit.
 221 2.0.0 Bye
 *** Closing transmission channe.
 *** Closing socket.

リファレンス:
Bash socket programming with /dev/tcp
gawk で SMTP を使ってメールを送信する
python で SMTP 認証を行ってメールを送信する