SMTP(Simple Mail Transfer Protocol)

SMTPは、IETFにより標準化されている電子メールの転送プロトコルであり、 インターネットではもっとも広く利用されています。ここではSMTPの動作を 説明した後に、SMTPを利用したメール送信プログラムについて解説します。

SMTPの動作

SMTPは、電子メールをメールサーバとメールクライアント間でやり取りをする プロトコルであり、メールサーバ間転送するときは送信元がメールクライアン トの役割を演じる。メールサーバとメールクライアント間のやり取りをしなが ら、メールの転送を実現するものであり、その手順はIETFによるRFC821に規定 されています。そのやり取りは、クライアントから送られるコマンドと、メー ルサーバの応答の組からなっており、これをRFC821に与えられた順番で実行し てメールの転送を行うものです。

その具体的な手順は4つのステップからできており、(1)メールの送り手側ホス トの情報を受け手側に送ります。(2)受け手側から受け手側ホストやメール転 送におけるデータ形式などについて指示を受けます。(3)データ、つまりメー ルを受け手側に送ります。(4)メールの中身を送り終わると互いにもとの状態 に戻ります。

なお、SMTPは(1)から(4)に交換されるデータや情報の具体的な伝送方式につい ては何も規定していません。つまり、データ自体を送る方法はTCPでもUDPでも 何でもいいのですが、すでに信頼性のあるストリーム型データ通信により、送 り手側ホストと受け手側ホストの間で通信が可能になっていることが前提とな ります。

インターネットではTCPにより送り手側ホストと受け手側ホストを接続するこ とが多いので、以下ではTCPを利用した場合を考えていきます。また、TCPで利 用されるポート番号は25番(10進数)を利用することが多いです。ただし、TCP である必要はありません。信頼性のあるデータ通信であれば何でもよいことに なっています。なお、ここで信頼性のあるデータ通信とは、通信失敗を発見し、 必要に応じて再送信を行い、また、データの受信順は送信順と一致するような 通信を指します。逆にいえばSMTP自体はデータ自体の転送失敗や順番の不一致 には対処できないことを意味します。

SMTPの手順

SMTPによるメール転送では、送り手側ホストは受け手側ホストに HELOMAILRCPTDATARSETSENDSOMLSAMLVRFYEXPNHELPNOOPQUITTURNなどのコマンドを先頭につけたメッセージを送 ります。一方、受け手側ホストは251551354 などの制御コードを先頭につけた返答メッセージを返します。以下にその手順 を示す。

コラム:SMTPコマンドとテスト

SMTPのコマンドはメール転送プログラムが使うものだが、人間がマニュアルで テストすることを考慮し、HELOSENDなどの英語に近いコ マンドをもっており、HELPコマンドも用意されていることが多い。

ステップ0:送信側ホストと受信側ホスト間の通信接続

SMTPの準備として、送信側ホストと受信側ホストをTCPにより通信接続する。 このとき、送信側ホストをTCPクライアント、受信側ホストをTCPサーバとし、 接続の通信ポート番号は25とします。

ステップ1:送信側ホストに関する情報を通知

SMTPの最初のステップは送信側が受信側HELOコマンドを送ることで す。これはSMTPを使ったメール転送が始まることを受け手側に知らせるもので す。

 送り手側:  HELO <sender_host> <CRLF>

ここでsender_hostには送り手側ホスト名となり、続く<CRLF> とは行末を表す特別な記号で、キャリッジリターンとラインフォールドの二つ の文字を続けたものとなります。C言語では"\r\n"と書かれる文字 列です。そして、受け手側は他のメールの転送中ではなく、新しいメールの転 送が介してできるときは以下のメッセージを送り手側に返します。

 受け手側:  250 OK reply

ここで、送り手側と受け手側の両方がメール転送の初期状態になり、いつでも メール転送を開始できることになります。

ステップ2:送信側ホストに関する情報を通知

SMTPの次のステップは送信側が受信側MAILコマンドを利用して、 送信側のメールアドレスを送ることです。

 送り手側:  MAIL FROM: <reverse-path> <CRLF>

ここでreverse-pathには送り手側のメールアドレスをいれます。 そして、受け手側が上記のメッセージを正常に受け取ると 以下のメッセージを送り手側に返します。

 受け手側:  250 OK reply

次に、送り手側ホストはRCPTコマンドを通じて宛先情報を受け手側 ホストにメッセージとして送ります。

 送り手側:  RCPT TO: <forward-path> <CRLF>

ここでforward-pathには送り先のメールアドレスをいれます。 そして、受け手側が上記のメッセージを正常に受け取ると以下のメッセージ を送り手側に返します。

 受け手側:  250 OK reply

受け手側がforward-pathを見つけることができないときは、 550 Failure replyを返します。

コラム:メッセージヘッダーフォーマット

ステップ3:メールデータの送信

DATAコマンドによりメールデータの送信を開始します。

 送り手側:  DATA <CRLF>

一方、受け取り側は次のメッセージを返します。

 受け手側: 354 Start mail input; end with <CRLF>.<CRLF>

354 Start mail input; end with <CRLF>.<CRLF>にお いて354はメールデータの受信準備ができたことを伝えるコードです。 <CRLF>.<CRLF>はメールデータ全体を送信したのちにそ の終了を通知するための文末を指定しています。つまり、送り手側はメールデー タを送り終えたら、そのあとに<CRLF>.<CRLF>を送り、 受け手側にメールデータの終了を伝えることになります。なお、 <CRLF>.<CRLF>はここでsender_hostには送り 手側ホスト名となり、続く<CRLF>とは行末を表す特別な記号で、キャリッ ジリターンとラインフォールドの二つの文字列でピリオドを挟んだ文字列です。 C言語で書きますと"\r\n.\r\n"のことです。

送り手側はメールデータを受け手側に送ります。そして最後には <CRLF>.<CRLF>をつけます。

 送り手側:  From: satoh@some.where.com
 to:
 ・・・・
 こんにちは、・・・・。
 それでは。<CRLF>.<CRLF>

受け手側は以下のメッセージを送り返して、 送り手側にメールデータが届いたことを知らせます。

 受け手側:  250 OK reply

コラム:メールの終わり

SMTPのDATAコマンドにおけるメールの終わりは「<CRLF>.<CRLF>」 で示される。もし、メールに「<CRLF>.<CRLF>」が含まれていた らどうなるのでしょうか。SMTPではメールに「<CRLF>.」が含まれてい たときは「<CRLF>..」に変換してから送信します。そして受信側では 「<CRLF>..」は「<CRLF>.」に変換します。つまり行頭にある「.」 は「..」に変換されるのです。それではなぜ、メール中の 「<CRLF>.<CRLF>」だけを「<CRLF>..<CRLF>」と変 換しないのはなぜでしょうか。これは、メール中にもとからあった 「<CRLF>..<CRLF>」と変換した結果である 「<CRLF>..<CRLF>」とが区別がつかなくなるからです。 ですから、行頭にピリオドがあったら送信側ではもう一つピリオドを追加し、 受信側では行頭のピリオドを一つ削除します。

ステップ4:終了通知

送り手側はQUITコマンドを送り、一連のメール送信を終了します。

 送り手側:  QUIT <CRLF>

一方、受け取り側は次のメッセージを返します。

 受け手側:  250 OK reply

これでメール転送の一連の流れは終了です。上記は最も簡単なものでしたが、 メーリングリストなどを利用した場合には上記以外のコマンドも必要になります。

メール送信例

ここではメール転送における、送り手側と受け手側のメッセージのやりとりを 時間順に見てみましょう。また、図にすると図?のようになります。

クライアント HELO <satoh@some.where.com><CRLF>
サーバ 250 OK
クライアント MAIL FROM:<satoh@some.where.com><CRLF>
サーバ 250 OK
クライアント RCPT TO:<smith@another.com> <CRLF>
サーバ 250 OK
クライアント DATA <CRLF>
サーバ 354 Start mail input; end with <CRLF>.<CRLF>
クライアント こんにちは
クライアント ところ、・・・ということで、
クライアント 返事をおまちしております
クライアント <CRLF>.<CRLF>
サーバ 250 OK
クライアント QUIT<CRLF>
サーバ 250 OK

コラム:メーリングリスト

コラム:メールが届かないとは

SMTPクライアントプログラム例

SMTPクライアントのプログラムを書いてみましょう。通信接続部分はTCPクラ イアントのプログラムをそのまま利用することができ、ここでプログラムを作 らなければいけないのSMTPのメッセージのやりとり部分だけです。そのプログ ラム例を下記にあげます(下記のプログラムはJava言語版です。これをC言語に 直すだけです。C言語に直すつもりだったのですが、ちょっと不具合が見つ かってその修正に手間取っています。原因はメールサーバの互換性にありそう。 それと、ほとんど「なりすましメール」送信ソフトと化していますが、 いいのでしょうか?)。


import java.net.*;

import java.io.*;

import java.util.*;



public class SMTP {

  static final int DEFAULT_PORT = 25;

  static final String CRLF = "\r\n";

  DataInputStream reply = null;

  PrintStream send = null;

  Socket sock = null;



  public static void main(String[] args) {

    if( args.length != 5 ) {

      System.out.println(

	 "Usage: java SMTP smtp-server sender receiver title content");

    }

    else {

      new SMTP(args[0], 25, args[1], args[2], args[3], args[4]);

    }

  }



  public SMTP(String host, int port, String sender, String receiver, 

	      String title, String content) {

    try {

      sock = new Socket(host, port);

      reply = new DataInputStream(sock.getInputStream());

      send = new PrintStream(sock.getOutputStream());

      String rstr = reply.readLine();

      sendMessage(sender, receiver, title, content);

    }

    catch (Exception e) {

      try {

	if (sock != null) {

	  sock.close();

	}

      }

      catch (IOException ex) {}

    }

  }

  

  public void sendMessage(String sender, String receiver,

			  String title, String message )

  throws IOException, ProtocolException {

    String rstr;

    String sstr;

    InetAddress local;

    try {

      String host = InetAddress.getLocalHost().getHostName();

      send.print("HELO " + host + CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("250")) {

	throw new ProtocolException(rstr);

      }

      send.print("MAIL FROM: "+sender+CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("250")) {

	throw new ProtocolException(rstr);

      }

      send.print("RCPT TO: "+receiver+CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("250")) {

	throw new ProtocolException(rstr);

      }

      send.print("DATA"+CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("354")) {

	throw new ProtocolException(rstr);

      }

      send.print("From: "+sender+CRLF);

      send.print("To: " + receiver+CRLF);

      send.print("Subject: " + title+CRLF);

      send.print("Comment: Unauthenticated sender"+CRLF);

      send.print("X-Mailer: Tiny SMTP Program"+CRLF);

      send.print(CRLF);

      send.print(message+CRLF);

      send.print(".");

      send.print(CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("250")) {

	throw new ProtocolException(rstr);

      }

      send.print("QUIT"+CRLF);

      send.flush();

      sock.close();

    }

    catch (IOException e) {

      sock.close();

    }

  }

}



重要なのはsendMessage()の部分だけです。それ以外は TCPクライアントのプログラムと同じです。以下では重要な部分だけ プログラムを解説します。

メール転送開始の通知

メール転送を開始することを受け手側に通知します。開始通知は "HELO hostname \r\n"というメッセージを送ります。こ こでhostnameは送り手側ホストのアドレスです。そして、その メッセージは"\r\n"で終わるようにします。なお、 送り手側ホストがsome.where.comというアドレスであれば、送られるメッセージは "HELO some.where.com \r\n"になります。


      String host = InetAddress.getLocalHost().getHostName();

      send.print("HELO " + host + CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("250")) {

	throw new ProtocolException(rstr);

      }

そして、受け手側ホストが正常にHELOコマンドを受け取ったときは、 制御コード250から始まる文字列を返します。 受け手側ホストからのメッセージが250で終わる場合は次に ステップに移り、250以外のときは失敗として処理を中止します。

HELOコマンドが成功したら、送り手側への返送用メールアドレスを 受け手側ホストに知らせます。 仮に返送用メールアドレスがsatoh@some.where.comならば MAIL FROM: satoh@some.where.com\r\nを知らせることになります。


      send.print("MAIL FROM: "+sender+CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("250")) {

	throw new ProtocolException(rstr);

      }

受け手側ホストから成功をしめす制御コード250を受け取ったら、 同様にそのメールの宛先アドレスを受け手側に知らせます。


      send.print("RCPT TO: "+receiver+CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("250")) {

	throw new ProtocolException(rstr);

      }

受け手側ホストから返答メッセージを受け取り、それの先頭が制御コード 250であることを確かめます。そして、メールデータの送信開始を示 すDATAコマンドを送ります。


      send.print("DATA"+CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("354")) {

	throw new ProtocolException(rstr);

      }

受け手側は制御コード354で始まるメッセージを返します。 そして、メールデータの先頭にヘッダー情報を付加します。 ヘッダー情報にはDateSubjectToCcFromなどがあります。また、 メールデータの終わりには終端を示す"\r\n.\r\n"をつけます。


      send.print("From: "+from_address+CRLF);

      send.print("To: " + to_address+CRLF);

      send.print("Subject: " + subject+CRLF);

      send.print(CRLF);

      send.print(message);

      send.print(CRLF);

      send.print(".");

      send.print(CRLF);

      send.flush();

      rstr = reply.readLine();

      if (!rstr.startsWith("250")) {

	throw new ProtocolException(rstr);

      }

受け手側ホストから返答メッセージを受け取り、それの先頭が制御コード 250であることを確かめます。そして、メール転送を終わりを通知す るQUITコマンドを送ります。


      send.print("QUIT"+CRLF);

      send.flush();

      sock.close();

実行方法はコマンドプロンプトから以下を入力します。


      % java SMTP smtp-server sender receiver title content

たとえば以下のように入力します。紙面の都合で折り返してありますが 一行のコマンドとして入力してください。


      % java SMTP some.where.com 

          someone@some.where.com another@other.where.com

             Hello "How are you? Good-bye"

コラム:なりすましメール

なりすましメールとは他人のメールアカウントや、送信者とは無関係の メールサーバを介して送ることです。

ここではtelnetを使ったなりすましメールの送り方を例示します。telnetコマ ンドを起動し、太字の行を入力し、行の終わりでは必ずエンターキー を押してください。


% telnet some.where.co.jp 25

Trying some.where.co.jp ... 

Connected to some.where.co.jp.

Escape character is '^]'.

220 some.computer.com Sendmail 4.1/SMI-4.1 ready at Fri, 13 Nov 98 11:10:10 MDT

HELO another.place.com

250 

MAIL FROM: jobs@some.computer.co.jp

250 ok

RCPT TO: gates@some.software.co.jp

250 ok

DATA

354 Enter mail, end with "." on a line by itself



Dear Gates

I would like to be grateful it if you could buy my company. 

Jobs



.





QUIT

250 ok

ここからいえることは、メールが届いても本当にそのメールアドレスの人から 届いたものなのかはわからない。したがってメールは信用できないということ です。