SMTPクライアントを作成して、Gmailを利用する
経緯
今年の10月より、ネットワークの勉強をしています。
使用しているのはComputer Networking: A Top-Down Approach, Global Editionです。
この本の2章に、「SMTPクライアントを作成しようぜ!」という問題がありました。
解いてみると、意外に苦労したので備忘録として残しておきます。
SMTPクライアントとは?
SMTPクライアントは大雑把にいうと、GmailやMozilla Thunderbirdのようなメーラーです。
まずSMTPは、メールを送信するプロトコル(約束事)です。
メールは利用しているPC(クライアント)から、ネットでつながっている送信先のPC(サーバー)に送信されます。
この時に使用されるのがSMTPです。
どう実装するか
TCPとUDP
まずクライアントと、サーバーは何らかの方法で通信しないといけません。
通信方法は2種類あります。
今回利用したのは、TCPです。(単純に指定されていただけ)
実はSMTPは、どちらを使用するかを規定していません。
ただUDPだと、通信内容がサーバー側に到達しない可能性があります。
SMTPについて
SMTPの内容については、RFC 5321という文書にまとまっています。
ただ適当に実装するだけなら、Wikipediaの内容で十分です。
つまり、詳細はWikipediaの内容を見ていただきたいのですが、下記コマンドを利用するだけでよいです。
- EHLO
- RCPT
- DATA
- QUIT
コマンドをどう利用するのか
上記のコマンドは、引数が必要です。(引数の内容はWikipedia見てください。)
利用するコマンド名と引数がわかれば、次は簡単です。
コマンド名と引数をそのまま、サーバーに送信するだけです。
ソース
実装したソースは下記となります。(教科書の指定がPythonとなっているので、Pythonで実装してます。)
import socket import sys SERVER_NAME = sys.argv[1] SERVER_PORT = sys.argv[2] YOUR_ADDRESS = sys.argv[3] TO_ADDRESS = sys.argv[4] MESSAGE = "\r\n I love computer networks!" END_MESSAGE = "\r\n.\r\n" SIZE = 1024 def send(client_socket, message): client_socket.send(message.encode()) def receive(client_socket, status_code): recv = client_socket.recv(SIZE).decode() if recv[:3] != str(status_code): print(str(status_code) + ' reply not received from server.') print(recv) # Choose a mail server (e.g. Google mail server) and call it mailserver mailserver = (SERVER_NAME, int(SERVER_PORT)) # Create socket called clientSocket and establish a TCP connection with mailserver clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) clientSocket.connect(mailserver) receive(clientSocket, 220) # Send HELO command and print server response. send(clientSocket, 'HELO Alice\r\n') receive(clientSocket, 250) # Send MAIL FROM command and print server response. fromCommand = 'MAIL FROM: <' + YOUR_ADDRESS + '>\r\n' send(clientSocket, fromCommand) receive(clientSocket, 250) # Send RCPT TO command and print server response. rcptCommand = 'RCPT TO: <' + TO_ADDRESS + '>\r\n' send(clientSocket, rcptCommand) receive(clientSocket, 250) # Send DATA command and print server response. send(clientSocket, 'DATA\r\n') receive(clientSocket, 354) # Send message data. send(clientSocket, MESSAGE) # Message ends with a single period. send(clientSocket, END_MESSAGE) receive(clientSocket, 250) # Send QUIT command and get server response. send(clientSocket, 'QUIT\r\n') receive(clientSocket, 221) clientSocket.close()
Gmailの利用
実は上記のソースは、自宅のメールサーバー等では動きますが、Gmailでは動きません。
じゃあどうすれば良いのかというと、下記が必要となります。
- 暗号化
- 認証
- Gmail側の設定
暗号化
上記は通信内容を暗号化していません。
なのでまず暗号化が必要となります。
Pythonの場合、sslモジュールを使用することで暗号化ができます。
認証
上記のコードをみて、「あれっ」と思った方もいるかもしれません。
実は上記のコードは、認証処理(要はログイン)を行っていません。
認証処理をどうすれば良いのかというと、「SMTP-AUTH」(RFC 4954)を利用するだけです。
いろいろ方法はあるのですが、簡単そうな実装方法(Auth Loginコマンド)を選びました。
実装方法は下記となります。
- サーバーにAUTH LOGINを送信
- サーバーにユーザー名を送信
- サーバーにパスワードを送信
Gmailの設定
実はGmailはセキュリティを担保するため、いろいろ設定を行っています。
そのためユーザー名とパスワードをサーバーに伝えるだけでは、利用できません。
具体的には下記手順が必要です。
コード
実装したコードは下記となります。
import socket import ssl import sys import base64 import time SERVER_NAME = 'smtp.gmail.com' SERVER_PORT = 465 USER = sys.argv[1] PASSWORD = sys.argv[2] FROM_ADDRESS = sys.argv[3] TO_ADDRESS = sys.argv[4] MESSAGE = "\r\n I love computer networks!" END_MESSAGE = "\r\n.\r\n" SIZE = 1024 def send(client_socket, message): client_socket.send(message.encode()) def receive(client_socket, status_code): recv = client_socket.recv(SIZE).decode() if recv[:3] != str(status_code): print(str(status_code) + ' reply not received from server.') print(recv) # Create socket called clientSocket and establish a TCP connection with mail server context = ssl.create_default_context() with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as bare_client: with context.wrap_socket(bare_client, server_hostname=SERVER_NAME) as client: client.connect((SERVER_NAME, SERVER_PORT)) receive(client, 220) send(client, 'EHLO example.com\r\n') receive(client, 250) send(client, 'AUTH LOGIN\r\n') receive(client, 334) send(client, base64.b64encode(USER.encode()).decode() + '\r\n') receive(client, 334) send(client, base64.b64encode(PASSWORD.encode()).decode() + '\r\n') receive(client, 235) # Send MAIL FROM command and print server response. fromCommand = 'MAIL FROM: <' + FROM_ADDRESS + '>\r\n' send(client, fromCommand) receive(client, 250) # Send RCPT TO command and print server response. rcptCommand = 'RCPT TO: <' + TO_ADDRESS + '>\r\n' send(client, rcptCommand) receive(client, 250) # Send DATA command and print server response. send(client, 'DATA\r\n') receive(client, 354) # Send message data. send(client, 'From user1 <' + FROM_ADDRESS + '>\r\n') send(client, 'To: user2 <' + TO_ADDRESS + '>\r\n') send(client, 'Date: ' + time.asctime(time.localtime(time.time())) + '\r\n') send(client, 'Subject: Test\r\n') send(client, '\r\n') send(client, MESSAGE) # Message ends with a single period. send(client, END_MESSAGE) receive(client, 250) send(client, 'QUIT\r\n') receive(client, 221)