Cloud DevOps Cookbook Part 4 - REST APIを使ってPythonでDevOpsのDirectMailを探る

今回は、**Python**で**REST API**を利用した**アリババクラウドDevOps**の**DirectMail**について探っていきます。

*本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/cloud-devops-cookbook-part-4-api-gateway-and-directmail-with-python-3-0_593732)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。*

 [Alibaba Cloud Tech Share](https://www.alibabacloud.com/campaign/techshare?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)、著者 John Hanley。 [Tech Share](https://www.alibabacloud.com/campaign/techshare?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)は、技術的な知識やベストプラクティスをクラウドコミュニティ内で共有することを奨励するAlibaba Cloudのインセンティブプログラムです。

ラピッドプロトタイピングや新しいクラウドサービスを学ぶための私のお気に入りの言語はPythonです。Pythonを使うと、ほとんどのタスクがとてもシンプルになるので、新しいAPIを数分でテストすることができます。これにより、特定の言語でAPIがどのように動作するかなどの細かなことを気にすることなく、学習に集中することができます。しかし、ミスをすると何百万ドルもかかってしまうようなエンタープライズレベルのコードには、C++を使っています。

Alibaba の [DirectMail](https://www.alibabacloud.com/product/directmail?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)はPythonをサポートしていません。PHP、Java、C#はサポートしています。[DirectMail](https://www.alibabacloud.com/product/directmail?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)の非常に一般的なユースケースは、動的なウェブサイトと静的なウェブサイトにメールを配信することです。動的なウェブサイトでは、PHPが最適です。静的なウェブサイトでは、[DirectMail](https://www.alibabacloud.com/product/directmail?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)と[Function Compute](https://www.alibabacloud.com/product/function-compute?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)を統合することは完璧です。しかし、Function ComputeはPHPをサポートしていません。これは、デスクトップでは1つの言語で開発し、クラウドでは別の言語で開発することを意味します。

そこで、DirectMailのREST APIを使うというアイデアが出てきました。それは、Pythonを使用して、REST APIを使用する方法と、特にあまりドキュメント化されていない署名を作成する方法を学ぶことができることを意味しています。

DirectMailは3種類のインターフェースをサポートしています（全ての言語に対応しているわけではありません）。

1、Alibaba REST API
2、Alibaba Cloud SDK
3、Alibaba SMTP Interface
前回の記事では、PythonでSDK([Function Compute](https://www.alibabacloud.com/product/function-compute?spm=a2c65.11461447.0.0.38ad725bFhEjCQ))を利用した例を紹介しました。今回はDirectMail REST APIに焦点を当て、Pythonでの実際の動作例を紹介します。

#アリババクラウドDirectMail REST API
REST APIを使う理由はいくつかあります。

1、低レベルのAPI、パラメータ、エラーを理解している。
2、コードスペースが小さい。
3、ロードと実行時間の短縮。
4、依存関係が少ない。
5、ターゲットシステムにSDKライブラリをインストールする必要がない。
6、SDKはターゲット言語では利用できません。

REST APIの必要条件
1、HTTPS、HTTPヘッダ、HTTPボディを理解していること。
2、HTTP HEAD、DELETE、GET、POST、PUTメソッドの違いを理解していること。
3、各HTTPメソッドのデータ送信方法を理解していること。
4、各 HTTP メソッドに含まれている必要がある API パラメータを理解していること。
5、HTTP リクエストの署名方法を理解していること。

#アリババクラウドの公開パラメータ
[DirectMail Publicのパラメータに関するアリババのドキュメント](https://www.alibabacloud.com/help/doc-detail/29440.htm?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)へのリンクです。

|名前| タイプ| 必須か |説明|
|:-----------|----|--------|:-----------|
|Format|String	|No|応答値のタイプ。JSONとXMLがサポートされています。XMLがデフォルトのフォーマットです。この例ではJSONを使用します。|
|Version|String|Yes|APIのバージョン番号。形式はYYYY-MM-DDです。RegionIDがcn-hangzhouの場合、バージョン番号は2015-11-23。RegionIDがcn-hangzhouでない場合のバージョン番号は、ap-southeast-1などの場合は2017-06-22です。例では2017-06-22を使用します。|
|AccessKeyId| String |Yes| Alibaba Cloudがサービスにアクセスするためにユーザーに発行するAccessKeyId。|
|SecurityToken| String| Depends| ユーザーに対して定義されたアクセス・キーを使用している場合は、このパラメータは必要ありません。ロールを使用している場合は、context.credentialsオブジェクトの一部として関数に渡されるセキュリティ・トークンです。|
|Signature	|String|	Yes	| 署名結果の文字列です。署名の計算方法については「[Signature](https://www.alibabacloud.com/help/doc-detail/29442.htm?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)」を参照してください。|
|SignatureMethod| String| Yes|署名方法。HMAC-SHA1 は現在サポートされています。|
|Timestamp |String| Yes |リクエストのタイムスタンプ。日付のフォーマットは[ISO8601](http://en.wikipedia.org/wiki/ISO_8601?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)標準に準拠し、UTC時間を採用します。形式は以下の通りです。YYYY-MM-DDThh：mm：ssZ。例：2015-11-23T04:00:00Z（北京時間2015年11月23日12:00:00の場合）。|
|SignatureVersion| String| Yes| 署名アルゴリズムのバージョン。現在のバージョンは1.0です。|
|SignatureNonce| String|Yes| 固有の乱数。リプレイ攻撃を防ぐために使用します。リクエストごとに異なる乱数を使用する必要があります。|
|RegionId| String| Yes |データセンター情報。cn-hangzhou、ap-southeast-1、ap-southeast-2は現在対応しています。|

上記のパラメータについてのコメント：

**SignatureNonce**: このパラメータにはしばらくの間、悩まされました。最初はこれが HMAC_SHA1 署名の際に使用されるソルト値だと思っていました。しかし、これはuuid.uuid4()によって生成され、ヘッダに含まれる文字列であることがわかりました。後続のコマンドでこの文字列の値を繰り返すと、そのコマンドは拒否されます。

**AccessKeyId**：DirectMail専用の権限を持つ新しいRAMユーザを作成します。この場合、REST API 用の Access Key と Access Key Secret の両方が必要になります。

**Timestamp**：システムの日付と時刻が正しいことが重要です。この時刻がアリババのサービスと異なる場合、リクエストは拒否されます。可能であれば、システムにNTPなどのタイムサービスを使用してください。

#Alibaba Cloud DirectMailリクエストパラメータ
[DirectMail Requestのパラメータに関するアリババのドキュメント](https://www.alibabacloud.com/help/doc-detail/29444.htm?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)へのリンクです。

|名前|タイプ| 必須か| 説明|
|:-----------|----|--------|:-----------|
|Action |String |Required| オペレーションインターフェース名、システム必須パラメータです。値を指定します。SingleSendMail。|
|AccountName| String |Required |コンソールで構成されている送信者アドレスです。|
|ReplyToAddress |Boolean| Required |コンソールで構成されている返信先アドレス（ステータスが「検証済み」である必要があります）。|
|AddressType |Number |Required |値の範囲：0-1。 0 はランダムなアカウントを示し、1 は送信者アドレスを示します。|
|ToAddress	|String|	Required |受信者のアドレス。複数のアドレスはカンマで区切ることができ、最大 100 個のアドレスをサポートします。|
|FromAlias| String |オプション| 送信者のニックネーム。ニックネームの長さは 15 文字以下にする必要があります。たとえば、送信者のニックネームは Daisy に設定され、送信者のアドレスは test@example.com です。受信者は、Daisy [test@example.com](test@example.com) のアドレスを参照します。|
|Subject| String| Optional| Subject (推奨)。
|HtmlBody|	String|	Optional	|HTMLでメール本文を表示します。|
|TextBody	|String|	Optional	|メール本文をテキストで表示します。|
|ClickTrace |String| Optional |値の範囲。0-1。 1 は受信者追跡を有効にすることを示します。0 は受信者追跡を有効にしないことを示します。このパラメータの既定値は 0 です。|

#正しい日付の計算

```
# Correctly formatted date and time
now = datetime.datetime.utcnow()

# Date used by the HTTP "Date" header
date = now.strftime("%a, %d %b %Y %H:%M:%S GMT")

# Date used by the API parameter "Timestamp"
utc = now.isoformat(timespec='seconds') + 'Z'
```

#この例のホストとエンドポイント (Singapore  --> ap-southeast-1)

```
# HTTP Host header
host = "dm.ap-southeast-1.aliyuncs.com"

# URL for POST
url = "https://dm.ap-southeast-1.aliyuncs.com/"
```

#パブリックパラメータのパラメータリストの構築

```
parameters = {}

# Add the DirectMail public request parameters
parameters["Format"] = "json"
parameters["AccessKeyId"] = credentials['AccessKey']
parameters["SignatureMethod"] = "HMAC-SHA1"
parameters["SignatureType"] = ""
parameters["SignatureVersion"] = "1.0"
parameters["SignatureNonce"] = get_uuid()
parameters["Timestamp"] = utc
parameters["Version"] = "2017-06-22"
parameters["RegionId"] = "ap-southeast-1"
```

#リクエストパラメータのパラメータリストの構築

```
# Add parameters that are always set
parameters["Action"] = "SingleSendMail"
parameters["AddressType"] = "1"
parameters["ReplyToAddress"] = "true"

# Add the DirectMail API parameters
parameters["AccountName"] = dm_account
parameters["FromAlias"] = dm_alias
parameters["ToAddress"] = to_list
parameters["Subject"] = subject
parameters["HtmlBody"] = body
parameters["textBody"] = body_text
```

#署名処理のためのリクエスト文字列を構築

```
def build_request_string(table):
    """ Build canonical list """
    items = sorted(iter(table.items()), key=lambda d: d[0])
    enc = my_urlencode(items)
    return enc
```

リクエスト文字列に関する注意事項

リクエスト文字列のパラメータは最初にソートされなければなりません。それから文字列は url エンコードされます。これは、各キー/値のペアに & 文字が付加されることを意味します。

最終的な結果は以下の例のようになりますが、もっと長くなります。

`AccessKeyId=LTAIQlgy6erobert&AccountName=test%40test.com&Action=SingleSendMail …`


#リクエスト文字列から署名を作成
[DirectMail Signatureに関するアリババのドキュメント](https://www.alibabacloud.com/help/doc-detail/29442.htm?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)へのリンクです。

```
# Build the request string for the signing process
params = build_request_string(parameters)

# Create the actual string to sign (method = "POST")
stringToSign = method + "&%2F&" + percentEncode(params)

Signature = sign(stringToSign, credentials['AccessKeySecret'])
```

#DirectMail REST APIを呼び出してメールを送信するプログラム
[sendEmail.zip](https://alicloud-common.oss-ap-southeast-1.aliyuncs.com/sendEmail.zip?spm=a2c65.11461447.0.0.38ad725bFhEjCQ&file=sendEmail.zip)をダウンロードします。

```
############################################################
# Version 1.00
# Date Created: 2018-05-26
# Last Update:  2018-05-27
# https://www.neoprime.io
# Copyright (c) 2018, NeoPrime, LLC
############################################################

""" Alibaba Cloud DirectMail REST API With Signing """

import base64
import datetime
import hmac
import hashlib
import urllib
import uuid
import json
import requests

# My library for processing Alibaba Cloud Services (ACS) credentials
import mycred_acs

# From the DirectMail Console
dm_account = "<enter your value here>"
dm_alias = "<enter your value here>"

debug = 0

def set_connection_logging():
    """ Enable HTTP connection logging """
    if debug is 0:
        return

    import logging
    import http.client as http_client

    http_client.HTTPConnection.debuglevel = 1

    # You must initialize logging, otherwise you'll not see debug output.
    logging.basicConfig()
    logging.getLogger().setLevel(logging.DEBUG)
    requests_log = logging.getLogger("requests.packages.urllib3")
    requests_log.setLevel(logging.DEBUG)
    requests_log.propagate = True
    return

def get_uuid():
    """ return a uuid as a signing nonce """
    return str(uuid.uuid4())

def percentEncode(path):
    """ Encode a URL """
    res = urllib.parse.quote(path)
    res = res.replace('+', '%20')
    res = res.replace('*', '%2A')
    res = res.replace('%7E', '~')
    return res

def my_urlencode(query):
    """ Encode a Query """
    res = urllib.parse.urlencode(query)
    res = res.replace('+', '%20')
    res = res.replace('*', '%2A')
    res = res.replace('%7E', '~')
    return res

def build_request_string(table):
    """ Build canonical list """
    items = sorted(iter(table.items()), key=lambda d: d[0])
    enc = my_urlencode(items)
    return enc

def sign(string, secret):
    """ Sign REST API Request """
    nsecret = secret + '&'

    h = hmac.new(
        bytes(nsecret, "utf-8"),
        bytes(string, "utf-8"),
        hashlib.sha1)

    #sig = base64.b64encode(h.digest())
    sig = str(base64.encodebytes(h.digest()).strip(), "utf-8")
    return sig

def sendEmail(credentials, subject, body, body_text, to_list):
    """ Send an email using Alibaba DirectMail """
    # HTTP Method
    method = "POST"

    # Correctly formatted date and time
    now = datetime.datetime.utcnow()
    date = now.strftime("%a, %d %b %Y %H:%M:%S GMT")
    utc = now.isoformat(timespec='seconds') + 'Z'

    # HTTP Host header
    host = "dm.ap-southeast-1.aliyuncs.com"

    # URL for POST
    url = "https://dm.ap-southeast-1.aliyuncs.com/"

    parameters = {}

    # Add the DirectMail public request parameters
    parameters["Format"] = "json"
    parameters["AccessKeyId"] = credentials['AccessKey']
    parameters["SignatureMethod"] = "HMAC-SHA1"
    parameters["SignatureType"] = ""
    parameters["SignatureVersion"] = "1.0"
    parameters["SignatureNonce"] = get_uuid()
    parameters["Timestamp"] = utc
    #parameters["Version"] = "2015-11-23"
    parameters["Version"] = "2017-06-22"
    parameters["RegionId"] = "ap-southeast-1"

    # Add parameters that are always set
    parameters["Action"] = "SingleSendMail"
    parameters["AddressType"] = "1"
    parameters["ReplyToAddress"] = "true"

    # Add the DirectMail API parameters
    parameters["AccountName"] = dm_account
    parameters["FromAlias"] = dm_alias
    parameters["ToAddress"] = to_list
    parameters["Subject"] = subject
    parameters["HtmlBody"] = body
    parameters["textBody"] = body_text

    # Build the request string for the signing process
    params = build_request_string(parameters)

    # Create the actual string to sign
    stringToSign = method + "&%2F&" + percentEncode(params)

    #print("String to Sign")
    #print(stringToSign)

    Signature = sign(stringToSign, credentials['AccessKeySecret'])
    #print("Signature", Signature)

    parameters["Signature"] = Signature

    headers = {
        'Date': date,
        'Host': host
    }

    set_connection_logging()

    print("Sending Email to", parameters["ToAddress"])
    r = requests.post(url, data=parameters, headers=headers)

    if r.status_code != 200:
        print("Error: Email Send Failed:", r.status_code)
        print(r.text)
        return 1

    #print(r.text)

    result = json.loads(r.text)
    print("Success: Request ID:", result['RequestId'])

    return 0

# Load the Alibaba Cloud Credentials (AccessKey)
cred = mycred_acs.LoadCredentials()

dm_subject = "Welcome to Alibaba Cloud DirectMail"

dm_body = "<h2>Welcome to Alibaba Cloud DirectMail<h2>You are receiving this email as part of a test program.<br /><br />Click for <a href='https://www.neoprime.io/info/alibaba/'>more information<a>.<br /><br /><a href='https://www.alibabacloud.com/'><img src='https://www.neoprime.io/info/alibaba/img/alibaba-600x263.png' alt='Alibaba' width='700'><a>"

dm_body_text = "Welcome to Alibaba Cloud DirectMail\nYou are receiving this email as part of a test program."

dm_to_list = "test@test.com, test2@test.com

sendEmail(cred, dm_subject, dm_body, dm_body_text, dm_to_list)
```

Python 3.xで実行する: python sendEmail.py

#アリババ文書

[Alibaba DirectMail Product Page](https://www.alibabacloud.com/product/directmail?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)
[Alibaba DirectMail Documentation](https://www.alibabacloud.com/help/product/29412.htm?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)
[Alibaba DirectMail Public parameters](https://www.alibabacloud.com/help/doc-detail/29440.htm?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)
[Alibaba DirectMail Request parameters](https://www.alibabacloud.com/help/doc-detail/29444.htm?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)
[Alibaba DirectMail Signature](https://www.alibabacloud.com/help/doc-detail/29442.htm?spm=a2c65.11461447.0.0.38ad725bFhEjCQ)

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*