To be a professional

プログラミング関係の情報

SMTPクライアントを作成して、Gmailを利用する

経緯

今年の10月より、ネットワークの勉強をしています。
使用しているのはComputer Networking: A Top-Down Approach, Global Editionです。 この本の2章に、「SMTPクライアントを作成しようぜ!」という問題がありました。
解いてみると、意外に苦労したので備忘録として残しておきます。

SMTPクライアントとは?

SMTPクライアントは大雑把にいうと、GmailMozilla Thunderbirdのようなメーラーです。 まずSMTPは、メールを送信するプロトコル(約束事)です。
メールは利用しているPC(クライアント)から、ネットでつながっている送信先のPC(サーバー)に送信されます。 この時に使用されるのがSMTPです。

どう実装するか

TCPUDP

まずクライアントと、サーバーは何らかの方法で通信しないといけません。
通信方法は2種類あります。

  1. TCP
  2. UDP

今回利用したのは、TCPです。(単純に指定されていただけ)
実はSMTPは、どちらを使用するかを規定していません。
ただUDPだと、通信内容がサーバー側に到達しない可能性があります。

SMTPについて

SMTPの内容については、RFC 5321という文書にまとまっています。
ただ適当に実装するだけなら、Wikipediaの内容で十分です。
つまり、詳細はWikipediaの内容を見ていただきたいのですが、下記コマンドを利用するだけでよいです。

  • EHLO
  • MAIL
  • 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では動きません。
じゃあどうすれば良いのかというと、下記が必要となります。

  1. 暗号化
  2. 認証
  3. Gmail側の設定

暗号化

上記は通信内容を暗号化していません。
なのでまず暗号化が必要となります。
Pythonの場合、sslモジュールを使用することで暗号化ができます。

認証

上記のコードをみて、「あれっ」と思った方もいるかもしれません。
実は上記のコードは、認証処理(要はログイン)を行っていません。
認証処理をどうすれば良いのかというと、「SMTP-AUTH」(RFC 4954)を利用するだけです。
いろいろ方法はあるのですが、簡単そうな実装方法(Auth Loginコマンド)を選びました。
実装方法は下記となります。

  1. サーバーにAUTH LOGINを送信
  2. サーバーにユーザー名を送信
  3. サーバーにパスワードを送信

Gmailの設定

実はGmailはセキュリティを担保するため、いろいろ設定を行っています。
そのためユーザー名とパスワードをサーバーに伝えるだけでは、利用できません。
具体的には下記手順が必要です。

  1. SMTPで利用するアカウントを作成(なければ)
  2. SMTPで利用するアカウントの、「2段階認証」を有効にする
  3. 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)

ソースコードについて

ソースコードココで公開中です。