KLab若手エンジニアの これなぁに?

KLab株式会社の若手エンジニアによる技術ブログです。phpやRubyなどの汎用LLやJavaScript/actionScript等のクライアントサイドの内容、MySQL等のデータベース、その他フレームワークまで幅広く面白い情報を発信します。

SMTP

SMTPコマンドを打ってみよう

takada-atです。こんにちは。 amo-k先輩に「自分SMTPコマンドなんて打ったことないッス」と言うと、「おまえも打てよ、な?」と、まるで後輩に煙草を薦める不良の先輩のような調子で、SMTPコマンドを打つようにすごまれました。 というわけで今日は、telnetとSMTPコマンドを使い、メーラーになった気持ちでメールを送信してみます。 SMTPといっても何だかわからないという方もおられるでしょうが、SMTPは「Simple Mail Transfer Protocol(単純なメール転送プロトコロル)」の略であり、メール送信のために定められた手続きのことです。要するに「この決まりを守っていればメールを送受信できるよー」というきまりのことです。 どんなメーラーもメールサーバーも基本的には、SMTPに従った動作を実装しています。 Wikipediaの記事にリンクをはっておきます。 -Simple Mail Transfer Protocol - Wikipedia 最新のSMTPプロトコルは、RFC 5321で標準化されています。 わたしも全部読んだことはないのですが、以下にリンクを載せておきます。RFC5321の日本語訳は見つけられませんでしたが、旧版のRFC 821には複数の日本語訳があるようです。 -RFC 5321 - Simple Mail Transfer Protocol -RFC日本語版リスト SMTPでメールを送信するには、メールサーバーにtelnetでアクセスし、SMTPコマンドを送っていけばよいようです。 実際にやってみましょう。 同期の honda-h に「ランチに行きませんか」というメールを出してみます。 ↓honda-h


以下入力したコマンドを、C:からはじまる行に、サーバーからの返答をH:からはじまる行に書きます。 まずtelnetコマンドを使いmail.example.comの25番ポートにログインします。 (アドレスはすべて架空のものです)。
# telnet mail.example.com 25
> 220 mail.example.com ESMTP
HELOコマンドを入力し、こちらのサーバー名を伝えます。
C:HELO localhost
H:250 mail.example.com
MAILコマンドを利用し、差し出しアドレスを伝えます。
C:MAIL FROM:takada-at@example.com
H:250 ok
RCPTコマンドを利用し、送り先アドレスを伝えます。
C:RCPT TO:honda-h@example.com
H:250 ok
メールの本文はDATAコマンドを使って送ります。「.」だけで終る行がメッセージの終了を意味します。 今回はマルチバイト文字を使わず、ローマ字で送ってみることにします。
C:DATA
H:354 Please start mail input.
C:
Subject: lunch
From: takada-at@example.com

honda-h san. gohan tabe ni ikimasyou.
.

H:250 Mail queued for delivery.
最後にQUITコマンドを利用し、コネクションを切断します。メールサーバーはちゃんと挨拶ができる子のようです。
C:QUIT
H:221 Closing connection. Good bye.
もう少し実験してみましょう。 HELO の際に、nothing.example.com と答え、noone@example.com という存在しないアドレスにメールを送ります。 DATA コマンド中の From: の値も、noone@example.com にしておきます。
220 mail.example.com ESMTP
HELO nothing.example.com
250 mail.example.com
MAIL FROM: takada-at@example.com
250 ok
RCPT TO: noone@example.com
250 ok
DATA:
354 Please start mail input.
Subject: aaa
From: noone@example.com
cccc

.
250 Mail queued for delivery.
QUIT
221 Closing connection. Good bye.
こういう入力の仕方でも、メーラーデーモンからのリプライが takada-at@example.com に届きました。 デーモンからの返信先は必ず MAIL FROMで指定したアドレスとなるようです。 以上です。簡単ながら、自分で打ってみると、ブラックボックスに見えていたメール送信の仕組みが、心で理解できた気がします。やったことのない方はぜひ一度試してみるとよいのではないでしょうか。

SMTPコマンドを使って手動でメールを送ってみよう

amo-kです。 さて、SMTPクライント絡みでSMTPの話題です。 聞くところによるとtakada-at以外はSMTPコマンドを打ったことがあるそうです。 ということで今回はtakada-atにSMTPコマンドを使って手動でメールを送ってもらうことにします。 では、takada-at、よろしくお願いします~

SMTPクライアントを書いてみた

amo-kです。phpでSMTPクライアント書きました~ 書いてみたきっかけ PHPだとmail()mb_send_mail()でメール送信できちゃうけどよくよく見てみると、SMTPコマンドのHELOコマンドやMAIL FROMコマンドに値を指定できないぽい。最後の引数に、MTAに渡すコマンドラインオプションを指定可能だが、これはMTAに依存するということだ。MAIL FROMコマンドの値を指定したくても、この最後の引数に指定するしかないということになる。 では、何処でHELOコマンドやMAIL FROMコマンドの値を設定しているかというと、php.iniのSMTPディレクティヴやsendmail_fromディレクティヴの値となる。(Windows版phpのみ)つまり、アプリケーションレベルでこれ等の値を指定したい場合に、明確に指定するIFが無いということだ。 あれやこれやと考える時間がもったいないので、この前HTTPクライアント書いたし、せっかくなのでSMTPクライアントも書いて見たw コード:
<?php
/**
 * SMTPクライアント
 */
class AN_SMTP extends AN_Base
{
    private $host;
    private $port;
    private $error_no;
    private $error_str;
    private $sock_ttl;
    private $to;
    private $from;
    private $subject;
    private $body;
    private $header;
    private $envelope_from;
    private $socket;

    /**
     * コンストラクタ
     *
     * @param String $host     SMTPサーバホスト名
     * @param String $port     SMTPサーバポート番号
     * @param String $sock_ttl ソケットタイムアウト値
     * @return なし
     */
    public function __construct($host, $port, $sock_ttl)
    {
        $this->host     = $host;
        $this->port     = $port;
        $this->sock_ttl = $sock_ttl;
    }

    /**
     * メール送信
     *
     * @param String $to            RCPT TO コマンド用
     * @param String $from          Fromヘッダ用($envelope_from省略時はMAIL FROMコマンドにも使用)
     * @param String $subject       Subjectヘッダ用
     * @param String $body          Subjectヘッダ用
     * @param String $header        任意のヘッダ
     * @param String $envelope_from MAIL FROMコマンド用
     * @return なし
     */
    public function mail($to, $from, $subject, $body, $header = "", $envelope_from = "")
    {
        if (!$to || !$from || !$subject || !$body)
        {
            throw new AN_Exception("引数が不正です。");
        }
        $this->to      = $to;
        $this->from    = $from;
        $this->subject = $subject;
        $this->body    = $body;
        $this->header  = $header;
        $this->envelope_from = $envelope_from ? $envelope_from : $from;

        $this->socketOpen();
        $this->sendSMTP();
    }

    /**
     * ソケット接続
     *
     * @param  なし
     * @return なし
     */
    private function socketOpen()
    {
        if (($this->socket = fsockopen($this->host, $this->port, $this->error_no, $this->error_str, $this->sock_ttl)) === false)
        {
            throw new AN_Exception("socket open error!");
        }
        $this->getReceiveMsg();
    }

    /**
     * SMTPコマンド送信
     *
     * @param  なし
     * @return なし
     */
    private function sendSMTP()
    {
        $this->sendSocketCommand(sprintf("HELO <%s>\r\n", $this->host));
        $this->sendSocketCommand(sprintf("MAIL FROM: <%s>\r\n", $this->envelope_from));
        $this->sendSocketCommand(sprintf("RCPT TO: <%s>\r\n", $this->to));
        $this->sendSocketCommand("DATA\r\n");
        $this->sendSocketCommand(sprintf("%s\r\n%s\r\n.\r\n", $this->getHeader(), $this->body));
    }

    /**
     * ソケット接続先サーバよりレスポンスメッセージ取得
     *
     * @param  なし
     * @return なし
     */
    private function getReceiveMsg()
    {
        if(($res = fgets($this->socket, 1024)) === false)
        {
            // クライアント応答通信エラー
            throw new AN_Exception("socket receive error!");
        }
        if(!preg_match("/(2|3)/", substr(str_replace("\r\n", "", $res), 0, 1)))
        {
            // サーバ応答通信エラー
            throw new AN_Exception("socket response error!");
        }
    }

    /**
     * ソケット接続先サーバへコマンド送信
     *
     * @param  String $command コマンド
     * @return なし
     */
    private function sendSocketCommand($command)
    {
        if (!$command)
        {
            throw new AN_Exception(sprintf("SMTPコマンドが不正です。: %s", $command));
        }
        fputs($this->socket, $command);
        $this->getReceiveMsg();
    }

    /**
     * ヘッダ文字列取得
     *
     * @param  なし
     * @return なし
     */
    private function getHeader()
    {
        $header  = "MIME-version: 1.0\r\n";
        $header .= sprintf("Date: %s\r\n",    $nowdate);
        $header .= sprintf("From: %s\r\n",    $this->from);
        $header .= sprintf("Subject: %s\r\n", sprintf("=?shift_jis?B?%s?=", base64_encode($this->subject)));
        $header .= $this->header;
        return $header;
    }
}
?>
 KLab若手エンジニアブログのフッター