Perl でネットワークのお勉強(SMTP編)

広告

広告

Perl でネットワークのお勉強(SMTP編)

最終更新
2005-07-21T00:00:00+09:00
この記事のURI参照
https://www.7key.jp/nw/study2.html#top

「Perl でネットワークのお勉強」第一弾の人気が意外と高く、 このままシリーズ化しようかと勝手に調子に乗っている次第ですが、 とりあえず第二弾としまして、「Perl でネットワークのお勉強(SMTP編)」を考えています。 つまりは、SMTP を使用して Perl からメールを送信しようという企画なのですが、今回は SMTP サーバとやり取りを行いますので、 SMTP の仕組みや、リクエストとレスポンスの中身が理解できていないと少し難しいかもしれません。 SMTP についてよく分からないという方は、 「ネットワークの話 SMTP とは」を参考にして下さい。

今回も第一弾と同様に、汎用使用ができるよう関数の形で考えていきます。

$boolean = &sendMail('SMTP サーバ名','宛先',
                   '送信者アドレス','メールの件名','メール本文');

このような関数を作成しておけば、ある程度柔軟に使用できるのではないでしょうか。 では、まず例のごとく大枠から。

sub sendMail{
    use Socket;
    
    my($smtp, $to, $from, $subject, $body) = @_;
    my($s_body, $port, $socket_add, $res, $req);

}

当然ですが、今回もソケットを使用しますので、冒頭の「use Socket;」は忘れないように記述して下さい。 そして、「@_」を使用して、この関数への引数を変数に代入します。 更に次の行で、計算用と言いますか一時ファイルと言いますか、とりあえず変数を用意しておきます。 それぞれを何に使うかはおいおい説明していきますので、今は見てみぬふりをしておいてください。

次に前準備としまして、メールとして送信するヘッダ、データ部分を作成しておきます。 これは、最終的に、SMTP サーバに向けて発信するメールの本体部分となっており、基本的な記述方法は決められています。 お使いになっているメーラーでヘッダの確認をされると分かると思いますが、概ね以下のような内容です。

X-Mailer: Key_MTP ver1.0
MIME-Version: 1.0
Content-Type: text/plain; charset=iso-2022-jp
From: 宛先アドレス
To: 送信者アドレス
Subject: メールの件名

メール本文

最終的にこの内容をSMTP サーバに送信するので、これを変数に入れておきましょう。

sub sendMail{
    use Socket;
    
    my($smtp, $to, $from, $subject, $body) = @_;
    my($s_body, $port, $socket_add, $res, $req);
    
    $s_body  = 'X-Mailer: Key_MTP ver1.0'. "\n";
    $s_body .= 'MIME-Version: 1.0'. "\n";
    $s_body .= 'Content-Type: text/plain; charset=iso-2022-jp'. "\n";
    $s_body .= 'From: '. $from. '<'. $from. '>'. "\n";
    $s_body .= 'To: '. $to. "\n";
    $s_body .= 'Subject: '. $subject. "\n";
    $s_body .= "\n";
    $s_body .= $body;

}

これで大まかなプログラムの方向がはっきりしてきましたよね。 まずソケットを作成し、SMTP サーバとそのソケットを使用して会話を行い、最終的にソケットに「$s_body」を書き込む。 これでメールを送信することができそうです。 では、第一弾の復習になりますが、ソケットを作成しましょう。 今回もまずソケットの前に構造体を作成したいのですが、ちょっと待って下さい。 使用するポート番号IP アドレスも今のところ分かっていませんよね。 まずは今与えられている値を駆使してこの二つを調べなければなりません。 手始めにポート番号ですが、Perl には便利な「getservbyname 関数」というものがあります。 この関数は、ローカルに保存されているプロトコル名とポート番号の対応表から、必要なサービスの使用ポート番号を調べることができます。 SMTP は、TCP を使用して通信を行いますので、ポート番号は以下の記述により求めることができます。

$port = getservbyname('smtp','tcp');

次にIP アドレスですが、SMTP サーバのドメイン名は分かっていますので、 DNS サーバにIP アドレスを問い合わせてやれば教えてくれるはずです。 ここでも Perl には便利な「inet_aton 関数」が用意されています。 この関数にドメイン名を引数として渡してやれば、後はOS 経由で DNS サーバに問合せ、IP アドレスを返してくれます。 これでやっと構造体が作成できますね。実際に記述してみましょう。

$socket_add = pack_sockaddr_in($port,inet_aton($smtp));

もうここまで来たら第一弾と同じです。 ソケットを作成し、サーバに接続させ、バッファリングを行わない設定にしてみましょう。

sub sendMail{
    use Socket;
    
    my($smtp, $to, $from, $subject, $body) = @_;
    my($s_body, $port, $socket_add, $res, $req);

    $s_body  = 'X-Mailer: Key_MTP ver1.0'. "\n";
    $s_body .= 'MIME-Version: 1.0'. "\n";
    $s_body .= 'Content-Type: text/plain; charset=iso-2022-jp'. "\n";
    $s_body .= 'From: '. $from. '<'. $from. '>'. "\n";
    $s_body .= 'To: '. $to. "\n";
    $s_body .= 'Subject: '. $subject. "\n";
    $s_body .= "\n";
    $s_body .= $body;

    eval{
        $port = getservbyname('smtp','tcp');
        $socket_add = pack_sockaddr_in($port,inet_aton($smtp));
        socket(SCK, PF_INET, SOCK_STREAM, 0) || die("ソケットの生成失敗 $!");
        connect(SCK, $socket_add ) || die("接続失敗 $!");
        select(SCK);
        $| = 1;
        select(STDOUT);
        
        
        close(SCK);
        select(STDOUT);
    };
    if($@){return "$@";}
    else  {return 0;}
}

ここで、第一弾では黙って通り過ぎました 「eval 関数」と「die 関数」について説明をしておきましょう。 まず、「eval 関数」ですが、これは引数を Perl のプログラムとして実行します。 つまり、「eval{」と「};」の間に挟まれている文章を、Perl プログラムとして認識させるのです。 いえ、おっしゃりたいことは良く分かります。 普通に命令を記述しても、Perl は Perl プログラムとして認識してくれます。 「eval 関数」なんか意味が無いではないか、と私も思っていました。 でもこの関数、使ってみるとかなり使い勝手が良いですよ。まず分かり易い例を一つ挙げます。

$hoge = '1+1';
$hoge = eval($hoge);
print $hoge;

上記の実行結果は「2」です。 eval 中の文字列を解釈して実行してくれるということは覚えておいて損はありませんし、上級者のソースを読むときに戸惑うことも少なくなるでしょう。 この機会にぜひマスターして下さい。

eval 関数」の主な使い方はそれだけではありません。 次に紹介する使い方こそ「eval 関数」の醍醐味とも言える(勝手に思っている)のではないでしょうか。 どんな使い方かと言いますと、「die 関数」と併せてエラー回避をする方法です。 「die 関数」を「eval 関数」の中で使用しますと、 エラーメッセージが、$@に入れられ、更に eval を中断して未定義値を返します。 つまり、致命的なエラーがプログラムの実行中に起こったとしても、そのエラーを難なく回避することができるのです。 例として、以下の命令を見てみましょう。

connect(SCK, $socket_add ) || die("接続失敗 $!");

この命令を eval 中で使用した際、connect に失敗したとしましょう。 その場合、「die("接続失敗 $!");」が実行され、「$@」に「接続失敗 エラー番号(エラー文字列)」が代入された上で、eval を中断します。 eval 内でエラーが一回も起こらなかった場合には、間違いなく「$@」は空文字列ですので、 eval の外からエラーが起こったかどうかを判断することもできますし、エラーの内容を特定することも容易です。 特にネットワークプログラムの際は、想定しきれないトラブルに巻き込まれることが多々あります。 eval を使用したエラートラップを常に心がけた方が良いかもしれませんね。

これでソケットの作成も完了し、SMTP サーバへの接続も完了しました。 後はソケットを使って SMTP サーバと会話をするだけです。 初っ端に作っていました「$res」にサーバからの返答を「$req」にサーバへのお願いをそれぞれ代入することとします。 で、実際に記述する前に SMTP のリクエストとレスポンスを確認しておきましょう。 ソケットでのやり取りは概ね以下の通りとなります。

クライアント側サーバ側
HELLOコマンド
250
MAILコマンド
250
RCPTコマンド
250 or 251
DATAコマンド
354
メール本文
ピリオドのみ
250
QUITコマンド
250

これを踏まえて、実際に最後までプログラムを記述してみます。

sub sendMail{
    use Socket;
    
    my($smtp, $to, $from, $subject, $body) = @_;
    my($s_body, $port, $socket_add, $res, $req);

    $s_body  = 'X-Mailer: Key_MTP ver1.0'. "\n";
    $s_body .= 'MIME-Version: 1.0'. "\n";
    $s_body .= 'Content-Type: text/plain; charset=iso-2022-jp'. "\n";
    $s_body .= 'From: '. $from. '<'. $from. '>'. "\n";
    $s_body .= 'To: '. $to. "\n";
    $s_body .= 'Subject: '. $subject. "\n";
    $s_body .= "\n";
    $s_body .= $body;

    eval{
        $port = getservbyname('smtp','tcp');
        $socket_add = pack_sockaddr_in($port,inet_aton($smtp));
        socket(SCK, PF_INET, SOCK_STREAM, 0) || die("ソケットの生成失敗 $!");
        connect(SCK, $socket_add ) || die("接続失敗 $!");
        select(SCK);
        $| = 1;
        select(STDOUT);

        $res = <SCK>;
        unless($res =~ /^220/){
            close(SCK);
            die("接続失敗 $!");
        }

        $req = "HELO $smtp\n";
        print SCK "$req";
        $res = <SCK>;
        $res =~ s/\x0D\x0A|\x0D|\x0A/\n/g;
        unless($res =~ /^220/){
            close(SCK);
            die("HELOコマンド失敗 $!");
        }

        $req = "MAIL FROM:$from\n";
        print SCK "$req";
        $res = <SCK>;
        $res =~ s/\x0D\x0A|\x0D|\x0A/\n/g;

        unless($res =~ /^250/){
            print SCK "RSET\n";
            close(SCK);
            die("MAILコマンド失敗 $!");
        }

        $req = "RCPT TO:$to\n";
        print SCK "$req";
        $res = <SCK>;
        $res =~ s/\x0D\x0A|\x0D|\x0A/\n/g;

        unless($res =~ /^25[0|1]/){
            print SCK "RSET\n";
            close(SCK);
            die("RCPTコマンド失敗 $!");
        }

        $req = "DATA\n";
        print SCK "$req";
        $res = <SCK>;
        $res =~ s/\x0D\x0A|\x0D|\x0A/\n/g;
        unless($res =~ /^250/){
            print SCK "RSET\n";
            close(SCK);
            die("DATAコマンド失敗 $!");
        }

        jcode::convert( \$s_body , 'jis' );
        $req = "$s_body\n.\n";
        print SCK "$req";
        $res = <SCK>;
        $res =~ s/\x0D\x0A|\x0D|\x0A/\n/g;
        unless($res =~ /^354/){
            print SCK "RSET\n";
            close(SCK);
            die("本文送信失敗 $!");
        }

        $req = "QUIT\n";
        print SCK "$req";
        close(SCK);
        select(STDOUT);
    };

    if($@){return "$@";}
    else  {return 0;}
}

途中で正規表現が出てきたり、「jcode」を使用してみたりと色々やっていますが、細かいことはさておき、 これで送信先に無事メールを送ることができるはずです。

以上で、Perl でネットワークのお勉強(SMTP編)は終了です。 この内容を読んで頂ければ、ソケットを使用することにより、簡単にサーバと会話ができることが分かっていただけたと思います。 重要なのは、まず各プロトコルの内容をしっかり理解することでしょうね。

広告

Copyright (C) 2005 七鍵 key@do.ai 初版:2005年07月21日 最終更新:2005年07月21日