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

# 経緯
今年の10月より、ネットワークの勉強をしています。
使用しているのは[Computer Networking: A Top-Down Approach, Global Edition](https://www.amazon.co.jp/Computer-Networking-Top-Down-Approach-English-ebook/dp/B01N8T0496/ref=tmm_kin_swatch_0?_encoding=UTF8&qid=&sr=)です。
この本の2章に、「SMTPクライアントを作成しようぜ!」という問題がありました。
解いてみると、意外に苦労したので備忘録として残しておきます。

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

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

1. TCP
1. UDP

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

## SMTPについて
SMTPの内容については、RFC 5321という文書にまとまっています。
ただ適当に実装するだけなら、[Wikipedia](https://ja.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP-AUTH)の内容で十分です。
つまり、詳細は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. 暗号化
1. 認証
1. Gmail側の設定

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

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

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

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

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

# ソースコードについて
ソースコードは[ココ](https://github.com/ToruTakefusa/computer_networking_a_top_down_approach/tree/master/03_SMTP)で公開中です。