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

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

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;
    }
}
?>

HTTPクライアントを書いてみた(C++)

takada-atです。 Rubyでソケットをいじっていたら、同じものをC/C++でも書いてみたくなりました。 そこで、C++でもHTTPクライアントに挑戦してみました。C/C++はよくわからないので、変なコードになっていると思いますが、遠慮なくつっこみをいただけるとうれしいです。 (そもそもコードが長すぎる気がします。。)

#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

using namespace std;

class Httpc{
    int BUFF_LEN;
public:
    int port;
    int soc;
    Httpc(){
        BUFF_LEN = 256;
        port = 80;
    }
    vector<string> parse_url(string url){
        int p = url.find("://");
        string path;
        url = url.substr(p+3);
        p = url.find("/");
        if(p==string::npos){
            path = "/";
        }else{
            path = url.substr(p+1);
            url = url.substr(0,p);
        }
        //cout << "host:" << url << " path:" << path << endl;
        vector<string> r;
        r.push_back(url); r.push_back(path);
        return r;
    }
    void openc(string url){
        vector<string> urls = parse_url(url);
        hostent *host;
        sockaddr_in addrin;

        soc = socket(AF_INET, SOCK_STREAM, 0);
        string reqh;
        host = gethostbyname( (urls[0]).c_str() );
        memset(&addrin, 0 ,sizeof(addrin));
        addrin.sin_family = AF_INET;
        addrin.sin_addr.s_addr = *(in_addr_t*)host->h_addr;
        addrin.sin_port = htons(port);
        connect(soc, (sockaddr*)&addrin, sizeof(addrin));
        reqh = "GET "+ urls[1] +" HTTP/1.0\r\n";
        reqh += "HOST: "+ urls[0]+ "\r\n";
        reqh += "\r\n\r\n";
        //cout << reqh << endl;
        send(soc, reqh.c_str(), reqh.length(), 0);
    }
    void closec(){
        close(soc);
    }
    void print(string url){
        this->openc(url);
        char buff[1024];
        char hh[1024];
        char s[1024];
        int r = 1;
        int state = 0;
        int i=0, p;
        if(r<0){
            state = 3;
        }
        while(r>0){
            r = read(soc, buff, sizeof(buff));
            if(state==0){
                for(i=0;i<sizeof(buff);++i){
                    if(state==0){
                        if(buff[i]=='\r' && buff[i+1]=='\n'){
                            ++i;
                            state = 1;
                        }
                    }else if(state==1){
                        if(buff[i]=='\r' && buff[i+1]=='\n'){
                            state = 2;
                            p = i;
                            break;
                        }else{
                            state = 0;
                        }
                    }
                }
                if(state==2)
                    cout << buff+p+2 << endl;
            }else if(state==2){
                write(1,buff,r);
            }

        }
    }
};
int main(int argc, char *argv[]){
    Httpc c;
    c.print(argv[1]);
}

HTTPクライアントを書いてみた(Ruby Socketクラス)

amo-kさんにつづき、私(takda-at)もHTTPクライアントを実装してみました。 まずはRubyのコードです。Rubyでは、socketというネットワークプログラミング用のライブラリが標準で用意されています。その中でもTCPSocketなどのクラスを利用するとHTTPクライアントなども非常に簡単につくれるのですが、今回は勉強のため、あえて低レイヤーなところから書いています。 socketライブラリの中でも、Socketクラスは、ソケットをシステムコールレベルで操作するための機能を提供しています。メソッド名などもシステムコールと同じ名前が採用されているようです。 Rubyリファレンスマニュアルの説明にはそこまで詳細な解説が無いので、LinuxなどのManPageも合わせて見た方が参考になります。 また今回は勉強のために、socketライブラリのソースコードも少しのぞいてみました。socketライブラリはC言語で書かれたRubyの拡張ライブラリです。最新の安定板であるRuby1.8.7では、ruby-1.8.7-p72/ext/socket/socket.c にソースコードがあります。 参考 -Socket - Rubyリファレンスマニュアル- -Manpage of SOCKET -Manpage of GETHOSTBYNAME 難しかったのはSocket::connectを呼び出し、接続を行なうところです。このメソッドの引数には、バイナリデータを文字列の形でわたします。C 言語のconnect関数には、引数として、sockaddr構造体というものをわたすのですが、Socket::conncetメソッドの場合、Rubyの側からCの構造体を文字列の形でわたしてやる必要があります。 Rubyで、データをバイナリ文字列に変換するにはArrayクラスのpackメソッドを利用します。 Rubyでバイナリデータを扱うプログラムを書いたのははじめてだったので、非常に勉強になりました。 参考 -Manpage of CONNECT -Array::pack - Rubyリファレンスマニュアル -packテンプレート文字列 - Rubyリファレンスマニュアル なお、Socket::conncetメソッドは、socket.cの中では以下のように定義されています。
static VALUE
sock_connect(sock, addr)
    VALUE sock, addr;
{
    rb_io_t *fptr;
    int fd;

    StringValue(addr);
    addr = rb_str_new4(addr);
    GetOpenFile(sock, fptr);
    fd = fileno(fptr->f);
    if (ruby_connect(fd, (struct sockaddr*)RSTRING(addr)->ptr, RSTRING(addr)->len, 0) < 0) {
	rb_sys_fail("connect(2)");
    }

    return INT2FIX(0);
}
ruby_connect(fd, (struct sockaddr*)RSTRING(addr)->ptr, RSTRING(addr)->len, 0) という箇所で、 Ruby文字列(RSTRING)を、sockaddr*にキャストしているようです。 以下が作成したHTTPクライアントのコードです。URLをGETしてプリントするだけです。
#!/usr/bin/ruby -Ku
##httpc.rb: Rubyによる簡易HTTPクライアント


require 'socket'

class Httpc
    @@defaultport = 80
    def initialize
        @url
        @socket
        @host
        @port = @@defaultport
        @path
    end
    def open url,&b
        @url = url
        #url を host と pathに分解
        /http:\/\/([^\/]+)(.*)/ =~ url
        @host = $~[1]
        @path = $~[2]
        @path = "/index.html" if !@path || @path==""

        #ソケットを作成します。
        #Manpage of SOCKET
        #第1引数はプロトコロルファミリの指定です。AF_INETはIPv4を示す定数です。
        #第2引数は通信方式の指定です。
        #第3引数に0を指定することで、プロトコルファミリに見合ったプロトコルが選択されます。
        @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)


        #ホスト情報の取得
        #Manpage of GETHOSTBYNAME
        host = Socket.gethostbyname(@host)

        #connectの引数に必要な sockaddr_in構造体を作成しています。
        #Array::Packの引数については以下を参照してください。
        ##packテンプレート文字列 - Rubyリファレンスマニュアル
        sockaddr_in = ([Socket::AF_INET, @port]).pack("s1 n1")
        sockaddr_in += host[3] + [].pack("x8")

        #接続
        @socket.connect(sockaddr_in)
        
        
        reqh = "GET #{@path} HTTP/1.0\r\n"
        reqh += "HOST: #{@host}\r\n"
        reqh += "\r\n\r\n"
        #リクエストヘッダ送信
        @socket.print(reqh)


        if(!b) #ブロックがなければソケットを返す
            return @socket
        else  #ブロックがあればブロックにソケットを渡して実行
            yield @socket
            @socket.close
        end
    end
    def print(url)
        puts url
        self.open(url) do |s|
            state = 0
            until(s.eof?)
                buff = s.gets
                $stdout.print buff if(state==1)
                if(state==0)
                    state=1 if(buff=="\r\n")
                end
            end
        end
    end
end



def main
    if(!ARGV[0])
        puts "usage: ruby #{__FILE__} URI"
    else
        url = ARGV[0]
        h = Httpc.new
        h.print url
    end
end


main if(__FILE__==$0)
 KLab若手エンジニアブログのフッター