経緯
今年の10月より、ネットワークの勉強をしています。
使用しているのはComputer Networking: A Top-Down Approach, Global Editionです。
この本の2章に、「SMTPクライアントを作成しようぜ!」という問題がありました。
解いてみると、意外に苦労したので備忘録として残しておきます。
SMTPクライアントとは?
SMTPクライアントは大雑把にいうと、GmailやMozilla Thunderbirdのようなメーラーです。
まずSMTPは、メールを送信するプロトコル(約束事)です。
メールは利用しているPC(クライアント)から、ネットでつながっている送信先のPC(サーバー)に送信されます。
この時に使用されるのがSMTPです。
どう実装するか
TCPとUDP
まずクライアントと、サーバーは何らかの方法で通信しないといけません。
通信方法は2種類あります。
- TCP
- UDP
今回利用したのは、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はセキュリティを担保するため、いろいろ設定を行っています。
そのためユーザー名とパスワードをサーバーに伝えるだけでは、利用できません。
具体的には下記手順が必要です。
- SMTPで利用するアカウントを作成(なければ)
- SMTPで利用するアカウントの、「2段階認証」を有効にする
- SMTPで利用するアカウントの、アプリパスワードを作成する。
コード
実装したコードは下記となります。
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)
ソースコードについて
ソースコードはココで公開中です。