Linux socket プログラミング/POP3 クライアントプログラム

目次

概要

こんどは POP3 のクライアントプログラムです。MTA(メール転送エージェント)へのメール送信機能は ありませんので、POP3 サーバからメールを読み込むだけのプログラムです。 見た目かなり汚くなってしまいましたが、要所要所を変えればいろいろなプログラムへも応用できるので はないかと思います。

プログラムのイメージ

このプログラムで使われる関数のイメージを以下に示します。コネクションを張ったあとには POP3 のコマンドをサーバに送信(send(2))して、サーバからしかるべきメッセージを受信(recv(2))すると言う流れです。

POP3 サーバこのプログラム
recv()USER ID 送信
<------------------------
send()
recv()PASSWORD 送信
<------------------------
send()
recv()LIST コマンド送信
<------------------------
send()
send()LIST受信
------------------------>
recv()
recv()RETR コマンド送信
<------------------------
send()
send()メッセージ受信
------------------------>
recv()

ソースコード

    1  /*
    2   * POP3 client program
    3   * mailp.c
    4   * cc mailp.c -lnsl -o mailp
    5   */
    6
    7  #include 
    8  #include 
    9  #include 
   10  #include 
   11  #include 
   12  #include 
   13  #include 
   14  #include 
   15
   16  #define  BUFMAX  256
   17  #define  PORT_NO 110
   18
   19  void read_msg();
   20  void print_err(char *);
   21
   22  extern int h_errno;
   23  int net;
   24  int nbyte;
   25  char r_msg[BUFMAX];
   26  char s_msg[BUFMAX];
   27  char input[6];
   28
   29  int
   30  main(int argc, char *argv[])
   31  {
   32      struct hostent *host;
   33      struct servent *serv;
   34      struct sockaddr_in sa;
   35      char p_list[] = "LIST\n";
   36      char p_user[] = "USER pop_user\n";
   37      char p_pass[] = "PASS pop_password\n";
   38      char p_quit[] = "QUIT\n";
   39
   40      if (argc != 2) {
   41          printf("Usage: %s hostname\n", argv[0]);
   42          exit(1);
   43      }
   44
   45      /*
   46       * Check Host name
   47       */
   48      if ((host = gethostbyname(argv[1])) == NULL){
   49          herror("gethostbyname()");
   50          close(net);
   51          exit(1);
   52      }
   53
   54      memset(&sa,0, sizeof(struct sockaddr_in));
   55      memcpy(&sa.sin_addr, host->h_addr, sizeof(struct in_addr));
   56
   57      sa.sin_family = AF_INET;
   58      sa.sin_port = htons(PORT_NO);
   59
   60      /*
   61       * Make socket
   62       */
   63      if((net = socket(AF_INET, SOCK_STREAM, 0)) < 0)
   64          print_err("socket()");
   65
   66      /*
   67       * Connect to the mail server
   68       */
   69      if (connect(net, (struct sockaddr *)&sa, sizeof(struct sockaddr)) < 0)
   70          print_err("connect()");
   71
   72      /*
   73       * Receive connection messages from server
   74       */
   75      memset(r_msg,0,BUFMAX);
   76      if( recv(net,r_msg,BUFMAX,0) < 0)
   77          print_err("recv()");
   78      printf("%s",r_msg);
   79
   80      /*
   81       * Send user name
   82       */
   83      nbyte = strlen( p_user );
   84      if( send(net,p_user,nbyte,0) < 0)
   85          print_err("send()");
   86
   87      memset(r_msg,0,BUFMAX);
   88      if( recv(net,r_msg,BUFMAX,0) < 0)
   89          print_err("recv");
   90      printf("%s",r_msg);
   91
   92      /*
   93       * Send password
   94       */
   95      nbyte = strlen(p_pass);
   96      if( send(net,p_pass,nbyte,0) < 0 )
   97          print_err("send()");
   98
   99      memset(r_msg,0,BUFMAX);
  100      if( recv(net,r_msg,BUFMAX,0) < 0)
  101          print_err("recv()");
  102      printf("%s",r_msg);
  103
  104      /*
  105       * Request mail list & Select letter number
  106       */
  107      while(1){  /* loop start */
  108          nbyte = strlen( p_list );
  109          if( send(net,p_list,nbyte,0) < 0 )
  110              print_err("send()");
  111          while(1){
  112              memset(r_msg,0,BUFMAX);
  113              if( recv(net,r_msg,BUFMAX,0) < 0)
  114                  print_err("recv()");
  115              printf("%s",r_msg);
  116              if ( strstr( r_msg, "\n.") !=  NULL) {
  117                  printf("\n");
  118                  break;
  119              }
  120          }
  121
  122          /*
  123           * Enter letter number
  124           */
  125          printf("Enter the number you want to read( q for quit)\n");
  126          if ( scanf("%5s", input) < 0)
  127              print_err("scanf()");
  128
  129          if ( input[0] == 'q' ) {
  130              nbyte = strlen( p_quit );
  131              if( send(net,p_quit,nbyte,0) < 0)
  132                  print_err("send()");
  133              memset(r_msg,0,BUFMAX);
  134              if( recv(net,r_msg,BUFMAX,0) < 0)
  135                  print_err("recv()");
  136              printf("%s\n",r_msg);
  137              /* Normal closure process */
  138              if (shutdown(net, 2) < 0)
  139                  print_err("shutdown()");
  140              close(net);
  141              exit(0);
  142          }
  143          if( (isdigit(input[0])) == 0){
  144              printf("Input error\n");
  145              continue;
  146          }
  147          read_msg();
  148      } /* loop end */
  149  }
  150
  151  /*********************************************************
  152   *  read_msg()
  153   *
  154   *  Retrieve and print email message from the mail server
  155   *********************************************************/
  156  void
  157  read_msg()
  158  {
  159      sprintf(s_msg,"RETR %s\n",input);
  160      nbyte = strlen(s_msg);
  161      if( send(net,s_msg,nbyte,0) < 0 )
  162          print_err("send()");
  163      while(1){
  164          memset(r_msg,0,BUFMAX);
  165          if( recv(net,r_msg,BUFMAX,0) < 0)
  166              print_err("recv()");
  167          printf("%s",r_msg);
  168          if( strstr(r_msg,"\n.") != NULL ){
  169              printf("\n");
  170              break;
  171          }
  172      }
  173      return;
  174  }
  175
  176  void
  177  print_err(char *func)
  178  {
  179      perror(func);
  180      close(net);
  181      exit(1);
  182  }

ソースファイル mailp.c

実行例

このプログラムの実行例を示します。 プログラム名の後に引数として POP3 サーバ名、または IP アドレスを入力します。USER ID と PASSWORD はプログラム中に宣言されていますので入力の必要はありません。本当は「From:」ヘッダとか「To:」ヘッダを検出して抜き出そうと思ったんですが、ヘッダーからメッセージまでそのまま出力してしまってます。まぁ、その辺は後で付け足そうと思っています。(文字列の操作が良く分からない)

% ./mailp mail.example.com
+OK POP3 mail v2001.80 server ready
+OK User name accepted, password please
+OK Mailbox open, 2 messages
+OK Mailbox scan listing follows
1 392
2 395
.

Enter the number you want to read.( q for quit)
1
+OK 392 octets					     
Return-Path: <root><root@mail.example.com>	     
Received: (from root@localhost)			     
by mail.example.com (8.8.7/8.8.7) id AAA21525	     
for pop_user; Thu, 1 Jul 1999 00:24:05 +0900	     
Date: Thu, 1 Jul 1999 00:24:05 +0900		     
From: root					     
Message-Id: <199906301524.AAA21525@mail.example.com> 
To: pop_user@mail.example.com			     
Subject: test1					     
X-UIDL: 0beb13dbeab34ae6e3549b3f5ea2872c	     
Status: RO					     
						     
This is a test mail from root.			     
Can you read?					     
.						     
						     
+OK 2 messages (787 octets)			     
1 392						     
2 395						     
.						     
						     
Enter the number you want to read( q for quit)	     
q						     
+OK Sayonara                                         

解説

以下にこのプログラムの説明をします。最初にサーバの名前を IP アドレスに変換して、前の「ポート検査」プログラムでやったように コネクションを張り、サーバに対してPOP3のコマンドを送信するという順序です。

   40      if (argc != 2) {
   41          printf("Usage: %s hostname\n", argv[0]);
   42          exit(1);
   43      }

ここではプログラムの起動時に引数を指定しています。プログラム名 + サーバ名 というのが必要な構文です。 サーバ名が指定されないと、「Usage」が表示されます。

   48      if ((host = gethostbyname(argv[1])) == NULL){
   49          herror("gethostbyname()");
   50          close(net);
   51          exit(1);
   52      }

48 行目ではコマンドの引数であるサーバ名から gethostbyname() 関数を使って hostent 構造体を得ています。 gethostbyname() では /etc/nsswitch.conf で設定しているネームサービスを利用して、ネームアドレスから IP アドレスの参照を行います。

  hosts:   files dns

たとえは nsswitch.conf が上記のようになっていれば最初に /etc/hosts ファイルを見に行き、もしここに該当するホスト名が記述されていなければ、/etc/resolv.conf に記述されている DNS サーバに問い合わせを行い、IP アドレスを得ます。

gethostbyname()
概要ホスト情報入手
インクルードファイル#include <netdb.h>
形式struct hostent *gethostbyname(const char *name);
引数const char *name: サーバ名が格納されている文字配列のポインタ
戻り値成功時: hostent 構造体にホスト情報をセットし、その構造体のポインタを返す。
エラー: h_errno 変数の値を返す。
   54      memset(&sa,0, sizeof(struct sockaddr_in));

54行目は sockaddr_in 構造体である「sa」に 0 つまり NULL をセットして初期化しています。
63〜70 行目までは「ポート番号調査」のところで説明したのとほぼ同じなので割愛します。

   80      /*
   81       * Send user name
   82       */
   83      nbyte = strlen( p_user );
   84      if( send(net,p_user,nbyte,0) < 0)
   85          print_err("send()");
   86
   87      memset(r_msg,0,BUFMAX);
   88      if( recv(net,r_msg,BUFMAX,0) < 0)
   89          print_err("recv");
   90      printf("%s",r_msg);

ここでは send()システムコールを使って p_user(= USER pop_user)という文字列を送ります。USER というのは POP3 のコマンドで、その後に続くアカウントでのログインの処理を要求します。
成功するとパスワードを要求するメッセージがサーバから届くので、recv() システムコールで受け取ります。

send()
概要メッセージの送信
インクルードファイル#include <sys/types.h>
#include <sys/socket.h>
形式int send(int s, const void *msg, int len, unsigned int flags);
引数int s : socket 記述子
const void *msg: 送信メッセージのバッファアドレス
int len: 送信メッセージのサイズ
unsigned int flags: 送信メッセージのフラグ
戻り値成功時: 送信されバイト数
エラー: -1


recv()
概要メッセージの受信
インクルードファイル#include <sys/types.h>
#include <sys/socket.h>
形式int recv(int s, void *msg, int len, unsigned int flags);
引数int s : socket 記述子
const void *msg: 受信メッセージのバッファアドレス
int len: 受信メッセージのサイズ
unsigned int flags: 受信メッセージのフラグ
戻り値成功時: 受信したバイト数
エラー: -1

95〜102 行目も同様です。プログラムの最初で宣言している p_pass をサーバに送信します。
107〜148 行目 は while() を使ってループしています。ここではサーバからメールのリストを取得し、ユーザに読みたいメールの 番号の入力を促しています。「q」という文字を入力しない限りずっとループします。

  104      /*
  105       * Request mail list & Select letter number
  106       */
  107      while(1){  /* loop start */
  108          nbyte = strlen( p_list );
  109          if( send(net,p_list,nbyte,0) < 0 )
  110              print_err("send()");
  111          while(1){
  112              memset(r_msg,0,BUFMAX);
  113              if( recv(net,r_msg,BUFMAX,0) < 0)
  114                  print_err("recv()");
  115              printf("%s",r_msg);
  116              if ( strstr( r_msg, "\n.") !=  NULL) {
  117                  printf("\n");
  118                  break;
  119              }
  120          }

ここでは p_list という文字列を送信しているのはこれまでと同じです。この p_list に含まれる「LIST」というコマンドをサーバに送るとサーバが保存しているメールの番号とバイト数を返してきます。いままでのコマンドではサーバからのメッセージは BUFMAX(=256 バイト)で受け取れますが、メールが大量にとどいている場合はこのバッファーサイズ以上のリストが送信されてくる場合もありますので、 while() を使って順次メッセージを受信しています。
while() の終了(break)する条件については少々無理があるのですが、POP3 では サーバからの メッセージの最後に ピリオド「.」 だけの行のデータが送られてくるので、それをメッセージの中に見つけた場合終了としています。116 行目の「"\n."」というのは「改行」+「ピリオド」が連続している場合という意味で、万が一メッセージの中に POP のやり取りと関係ないピリオドが含まれていた場合でも break してしまわないように、必ず改行の後(行の先頭)にピリオドがある場合のみを条件にしています。

  122          /*
  123           * Enter letter number
  124           */
  125          printf("Enter the number you want to read( q for quit)\n");
  126          if ( scanf("%5s", input) < 0)
  127              print_err("scanf()");
  128
  129          if ( input[0] == 'q' ) {
  130              nbyte = strlen( p_quit );
  131              if( send(net,p_quit,nbyte,0) < 0)
  132                  print_err("send()");
  133              memset(r_msg,0,BUFMAX);
  134              if( recv(net,r_msg,BUFMAX,0) < 0)
  135                  print_err("recv()");
  136              printf("%s\n",r_msg);
  137              /* Normal closure process */
  138              if (shutdown(net, 2) < 0)
  139                  print_err("shutdown()");
  140              close(net);
  141              exit(0);
  142          }
  143          if( (isdigit(input[0])) == 0){
  144              printf("Input error\n");
  145              continue;
  146          }

126行目では scanf()でユーザからのメール番号の入力を待ちます。
129〜142 行目では、もし「q」という文字が入力された場合サーバに p_quit という文字列を送信しています。 その後サーバからの終了メッセージを受け取って、socket の終了処理にいって終わりです。

  143          if( (isdigit(input[0])) == 0){
  144              printf("Input error\n");
  145              continue;
  146          }
  147          read_msg();

「q」以外の文字が入力された場合にはまず、isdigit(3) 関数を使って文字が数字であるかどうかを判定しています。数字以外であった場合にはエラーを表示し、メールのリストを再表示します。数字であった場合には 147 行目で read_msg() 関数に飛びます。

  156  void
  157  read_msg()
  158  {
  159      sprintf(s_msg,"RETR %s\n",input);
  160      nbyte = strlen(s_msg);
  161      if( send(net,s_msg,nbyte,0) < 0 )
  162          print_err("send()");
  163      while(1){
  164          memset(r_msg,0,BUFMAX);
  165          if( recv(net,r_msg,BUFMAX,0) < 0)
  166              print_err("recv()");
  167          printf("%s",r_msg);
  168          if( strstr(r_msg,"\n.") != NULL ){
  169              printf("\n");
  170              break;
  171          }
  172      }
  173      return;
  174  }

159 行目では 126 行目でユーザから受け取ったメッセージ番号を例えば「RETR 2」という文字列にして 変数 s_msg に代入しています。 これ以降は上のメールリストの取得と同じです。256 バイトで受け取れきれないメッセージを while() で逐次 recv() し、「改行」+「ピリオド」がきたら終了します。
メッセージの表示が終了したら 107 行目の メッセージリストの表示に戻ります。
このプログラムではおそらく日本語の表示に端末(shell 端末)側の locale が大きく影響します。文字コードによっては日本語が表示できないのは必至です。また送られてきたメッセージの本文の中にピリオドから始まる文字があった場合(あまり無いとはおもいますが)、受信を終了してしまったりと 欠点を挙げれば枚挙にいとまがありません。 あくまで私の勉強のためのものであるとご理解下さい。 でも、少しでも読んでいる方でこれから勉強しようとしている方のお役に立てれば幸いです。


トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2022-01-03 (月) 20:16:20