ネットワークプログラムをまともに書いたこともなければ知識もないので、SMTPを題材にしてプログラムを書いてみる。
プログラムの概要
最低限の動作をするSMTPクライアントのプログラムはだいたい次のようになるはず。
(use gauche.net) ; ソケットを使うため (call-with-client-socket (make-client-socket 'inet host port) (lambda (in out) ; receive 220 ; send EHLO 送り手側のドメイン -> receive 250 ; send MAIL FROM: アドレス -> receive 250 ; send RCPT TO: アドレス -> receive 250 ;(必要なら send RCPT TO: ... を繰り返す) ; send DATA -> receive 354 ; send メールの各行(行頭のピリオドはエスケープする) ; send . -> receive 250 ; send QUIT -> receive 221 ))
実装する……
すぐに書けるかと思ったら、まったく思い通りにならず、書くのにやたらと時間がかかった。一応動くようになったけど、ほんとにちゃんと動作しているのか怪しい。こういうプログラムに対する適切なテストの仕方が知りたい。
できあがったプログラム
メインの手続きはsend-mailで、これを呼びだせばメールを送ることができる。
(use gauche.net) (define (send-mail host port source mail-from mail-to . options) (let-keywords options ((mail-tos '()) (helo-domain (sys-gethostname))) (call-with-client-socket (make-client-socket 'inet host port) (lambda (in out) (send-and-receive in out #f 220) (send-and-receive in out (format #f "EHLO ~a" helo-domain) 250) (send-and-receive in out (format #f "MAIL FROM: <~a>" mail-from) 250) (let1 recipients (cons mail-to mail-tos) (for-each (lambda (rcpt) (send-and-receive in out (format #f "RCPT TO: <~a>" rcpt) 250)) recipients)) (send-and-receive in out "DATA" 354) (send-body-of-mail out source) (send-and-receive in out "." 250) (send-and-receive in out "QUIT" 221) )))) (define (display-log . args) (apply format #t args)) (define (send-and-receive in out command expected-code) (when command (display-log "\n->~s\n" command) (send-line out command)) (receive (code lines) (receive-response in) (display-log "~s<-~s\n" expected-code lines) (when (not (= code expected-code)) (errorf "smtp error: expected-code ~a\n~a -> ~s" expected-code command lines)) lines)) (define (receive-response in) (let* ((lines (read-response in)) (code (get-code lines))) (values code lines))) (define (read-response in) (do ((lines '() (cons line lines)) (line (read-line in) (read-line in))) ((not (continuation-line? line)) (reverse (cons line lines))) ;no-body )) (define (get-code lines) (string->number (substring (car lines) 0 3))) ;; コードは各桁ごとに意味があるみたいなので、 ;; 数値に変換せず文字列のままの作りにした方が良いかもしれない。 (define (continuation-line? line) (and (> (string-length line) 3) (char=? (string-ref line 3) #\-))) (define (send-line out line) (display line out) (display "\r\n" out)) (define (send-body-of-mail out source) (define (send-additional-period-if-need out line) (when (and (> (string-size line) 0) (char=? #\. (string-ref line 0))) (display "." out))) (port-for-each (lambda (line) (send-additional-period-if-need out line) (send-line out line)) (lambda () (read-line source))))
次のような感じでsend-mailを呼べばメールを送ることができる。
(send-mail "somemailserver.zzz.co.jp" 25 (open-input-string "From: <from@aaa.bbb.ccc.co.jp>\n\ To: <to@xxx.yyy.zzz.co.jp>\n\ Subject: test mail\n\ \n\ This is Test Mail!\n\ \n\ .\n\ ...\n\ ...\n\ .\n") "from@aaa.bbb.ccc.co.jp" "to@xxx.yyy.zzz.co.jp" :mail-tos '("other1@xxx.yyy.zzz.co.jp" "other2@xxx.yyy.zzz.co.jp"))
いくつか注意。
- SMTPの標準のポート番号は25番。認証なんかをおこなうSMTPサーバは587番を使うみたいだけど、このプラグラムは認証を扱っていない。
- 引数sourceは入力ポートで、メールの内容(ヘッダ含む)が入っていること。ヘッダと本文の間には空白行を一行入れる。
- 入力ポートのリストも扱えるようにすると、ヘッダは文字列ポートで与えて、本文はファイルポートで与えるみたいなことができて便利かもしれない。単にわかりにくくなるだけかもしれない。
- SMTPサーバが、EHLOで提示したサーバが実際に存在するかどうかをチェックしている場合がある。その場合はオプション:helo-domainでちゃんとした(公に確認できる)サーバ名を与える。