ニフクラ ブログ

ニフクラ/FJ Cloud-Vやクラウドの技術について、エンジニアが語るブログです。

ニフクラESS APIでメール配信を効率的に行う方法

こんにちは、ニフクラESSチームです。

ニフクラではメール配信を簡単かつ高速・確実に行えるESS(Email Sending Service)という機能を提供しています。 ニフクラESSを使うとメールサーバーの構築に関わる工数や運用コストが削減できるだけでなく、メールの到達性の改善、携帯キャリア毎の配信最適化、スパム対策といった配送制御の効率化も図ることができます。

今回のチュートリアルでは、メール配信に課題を抱えているお客様向けに、ニフクラESSのAPIを利用してメールを送信する方法を紹介します。

1 前提知識

本チュートリアルは、以下の前提知識がある方を想定しています。

  • ニフクラの基本的なコントロールパネルの操作
  • HTTPリクエストに関する基礎知識
  • Pythonの基礎知識

2 事前準備

ニフクラESS APIでメールを送信するには以下の事前準備が必要となります。

  • 送信元のメールアドレスをニフクラに登録する (参考リンク)
  • ユーザーの2つの認証情報:アクセスキーとシークレットアクセスキーを取得する(参考リンク)
  • Pythonの実行環境を用意する(今回はPython 3.6.5を利用)

3 検証内容

3.1 ニフクラESS APIの利用方法

ニフクラESS APIにリクエストを送るためにはHTTPリクエストとして以下の4つの情報が必要となります。

  1. ニフクラESS APIのエンドポイント
  2. リクエストのメソッド
  3. リクエストの本文
  4. リクエストのヘッダー

それぞれの情報について、具体的な指定方法とPythonのコード実装も交えつつを説明していきます。

3.1.1 ニフクラESS APIのエンドポイント

ニフクラESS API及び他のサービスのエンドポイントはこの参考リンクで確認することができます。現在ニフクラESS APIのエンドポイントは以下となります。

 https://ess.api.nifcloud.com

3.1.2 リクエストのメソッド

例として、今回はメールを送信するSendEmail API を使ってみます。 SendEmail APIはPOSTメソッドで呼び出しています。

3.1.3 リクエストの本文

SendEmailを使うためにはいくつかのリクエストパラメーターが必須(詳細はクラウドAPIのESS:SendEmailのページを参照)となります。また、パラメーターの値はURLエンコードする必要があります。今回は以下のパラメーターを用います。

  • Action=SendEmail
  • Source=【送信元のメールアドレス】
  • Destination.ToAddresses.member.n=【送信先のN番目のメールアドレス】
  • Message.Subject.Data=【メールの件名】
  • Message.Body.Text.Data=【メール本文のテキストデータ】

Sourceに指定するメールアドレスは事前準備項目としてニフクラに登録済みのものです。

import urllib.parse

# SendEmail APIのパラメーターの例
api_name = "SendEmail"
source_address = urllib.parse.quote("sender@example.com")
destination_address = urllib.parse.quote("receiver@example.com")
email_subject = urllib.parse.quote("テストメール")
email_body = urllib.parse.quote("メール送信のテストなので返信が不要です")

上記のパラメーターを用いたリクエスト本文は以下となります。

request_payload = f'Action=SendEmail&Source={source_address}&Destination.ToAddresses.member.1={destination_address}&Message.Subject.Data={email_subject}&Message.Body.Text.Data={email_body}&Version=2010-12-01'

GETを利用するなどリクエストの本文がない場合にはempty string(長さ0の文字列)を指定します。

3.1.4 リクエストのヘッダー

ニフクラのAPIにリクエストするときにヘッダーの部分にはユーザーが署名するためのシグネチャーという情報を含む必要があります。署名というのはそのリクエストが本当にそのユーザーが送ったリクエストなのかを確かめるプロセスです。ニフクラESS APIはリクエストを署名する方法としてシグネチャーバージョン3と4(参考リンク)に対応します。今回はシグネチャーバージョン4を用いてリクエストを送ります。 シグネチャーバージョン4のヘッダー情報に必須なフィールドは、「X-Nifty-Date」と「Authorization」です。 Authorizationフィールドは、以下のテンプレートで設定することができます。ここで、ユーザー情報のアクセスキーとシークレットアクセスキーが必要となります。

Authorization:<ハッシュ化アルゴリズム>
Credential=<アクセスキー>/<CredentialScope>,
SignedHeaders=<SignedHeaders>, Signature=<シグネチャー>
  • Authorizationヘッダーのハッシュ化アルゴリズムをNIFTY4-HMAC-SHA256に設定します。
  • CredentialScopeリクエスト日付(YYYYMMDD)/リージョン/サービス識別子/nifty4_requestという形に指定します。ニフクラESS APIの場合はリクエスト日付/east-1/email/nifty4_requestです。
  • SignedHeadersはヘッダーパラメーターのパラメーター名のリストを指定します。 ヘッダーフィールド名は小文字に変換する必要があります。ニフクラESS APIの場合はhost;x-nifty-dateに指定します。
  • シグネチャーバージョン4を計算する方法の概要は以下の図で表します。それぞれの計算ステップの詳細はここで省略します。詳しく知りたい方は記事の最後にある付録を参考してみてください。

f:id:TuanNguyen:20191217114025j:plain
図1. シグネチャーバージョン4の計算

  • POSTの場合はContent-Typeヘッダーをapplication/x-www-form-urlencodedと設定します。
# ヘッダー情報作成の例
authorization_headers = "NIFTY4-HMAC-SHA256" + ' ' + f"Credential={access_key}/{credential_scope},SignedHeaders=host;x-nifty-date,Signature={signature}"
headers = {"x-nifty-date":nifty_date, "Authorization":authorization_header, "content-type": "application/x-www-form-urlencoded"}

3.1.5 リクエスト作成・送信

3.1.1 ~ 3.1.4 で準備したリクエスト情報を用いて、SendEmailのAPIにリクエストしてみます。以下はメールを送信するリクエストのサンプルです。

POST / HTTP/1.1
Authorization: NIFTY4-HMAC-SHA256 Credential=12345678901234567890/20190101/east-1/email/nifty4_request, SignedHeaders=host;x-nifty-date, Signature=cb82bacfc8a5eda43379d4f973aa1ef29bc74eb5dcad9f17649f1791f4592605
Host: ess.api.nifcloud.com
X-Nifty-Date: 20190101T000000Z
Content-Type: application/x-www-form-urlencoded
Content-Length: 192

Source=sender%40example.com&Destination.ToAddresses.member.1=receiver%40example.com&Action=SendEmail&Message.Body.Text.Data=%E3%83%86%E3%82%B9%E3%83%88%E3%83%A1%E3%83%BC%E3%83%AB&&Version=2010-12-01&Message.Subject.Data=%E3%83%86%E3%82%B9%E3%83%88%E3%81%AA%E3%81%AE%E3%81%A7%E8%BF%94%E4%BA%8B%E3%81%8C%E4%B8%8D%E8%A6%81%E3%81%A7%E3%81%99%E3%80%82

Pythonのrequestモジュールを使ってリクエストをしてみます。(pip install requestでインストールすることができます)

import requests
r = requests.post(url=api_endpoint, data=request_payload, headers=headers)
print('\nRESPONSE++++++++++++++++++++++++++++++++++++')
print(f"Response code: {r.status_code}\n")
print(f"Response Body: \n{r.text}")

# 結果の例
RESPONSE++++++++++++++++++++++++++++++++++++
Response code: 200

<SendEmailResponse>
  <SendEmailResult>
    <MessageId>xxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-xxxxx</MessageId>
  </SendEmailResult>
  <ResponseMetadata>
    <RequestId>xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</RequestId>
  </ResponseMetadata>
</SendEmailResponse>

4 応用編

4.1 メールの配信ログの取得

ニフクラESSにはGetDeliveryLogというAPIがあります。用意されたパラメーターを用いて条件を指定すると、それに一致する配信ログを取得することができます。例えば、2019年12月15日9時から2019年12月15日10時までの送信成功した配信ログを取得したい場合、以下のようにリクエスト本文を指定します。

request_payload = 'Action=GetDeliveryLog&StartDate=2019-12-15T09%3A00&EndDate=2019-12-15T10%3A00&Status=1&Version=2010-12-01'

StartDateはリクエスト時点の90日前まで、EndDateStartDateから24時間未満まで指定可能です。レスポンスの例は以下になります。

<GetDeliveryLogResponse>
  <GetDeliveryLogResult>
    <LogCount>1</LogCount>
    <Log>2019-12-15 09:23:26 sent 250 b101.repica.jp.1576574604051933 xxxxx@xxxxx.xxx xxxxxxx@xxxxx.xxx 250_2.0.0_OK__1576574606_l8si14262208pff.220_-_gsmtp</Log>
    <Log>2019-12-15 09:23:28 sent 250 b101.repica.jp.1576641380722134 xxxxx@xxxxx.xxx xxxxxxx@xxxxx.xxx 250_ok_dirdel</Log>
    <Log>2019-12-18 09:33:44 sent 250 b101.repica.jp.1576641846141682 xxxxx@xxxxx.xxx xxxxxxx@xxxxx.xxx 250_ok:__Message_8722050_accepted</Log>
  </GetDeliveryLogResult>
  <ResponseMetadata>
    <RequestId>d8cac4a5-3243-44f6-8c4f-0cba0fbc8d11</RequestId>
  </ResponseMetadata>
</GetDeliveryLogResponse>

すでにご利用いただいている皆様も他の条件で指定して、配信ログを取得してみてください。

4.2 添付ファイル付メール送信

SendEmailは簡単な件名と本文のメールを送信することができます。 しかし、添付ファイルが必要など複雑なメールの送信には使用できません。 その時、SendRawEmailというAPIを用います。基本的にSendEmailと似たような使い方ですが、 メール送受信先・件名・本文・添付ファイルをまとめてメールのマルチパートメッセージ形式に記述し、RawMessage.Dataのパラメーターに設定する必要があります。 マルチパートメッセージ形式は以下のように複数のヘッダー・空行・ボディから構成されます。

f:id:TuanNguyen:20191217114014j:plain
図2. マルチパートメッセージ形式

メール(email)は英語圏で生まれたシステムなので基本的にASCII文字(7bit)しか扱わない仕組みとなっています。 そのため、日本語や画像データなど非ASCIIなものを送信するのに、すべて英数字で表現できるようにエンコーディングする必要があります。 例えば、件名に日本語を使いたいときに、文字コードをUTF-8を利用する場合、UTF-8から7bitの文字に変換しなければなりません。 その変換方式としてBase64(アルファベット(a~z, A~z)と数字(0~9)、一部の記号(+,/)の64文字で表す)という方式があります。 件名の記述は以下のように書きます。

Subject: =?utf-8?B?44OG44K544OI44Oh44O844Or?=
# =?<文字コード>?<変換方式>?<変換した後の文字列>?= という形になります。

例えば、UTF-8文字列をBase64に変換するPythonのコードは以下となります。

import base64
text = "テストメール".encode('utf-8')
text_base64 = base64.b64encode(text)

日本語が含まれるメールの本文には同様に文字コードと変換方式を指定して、以下のように記述します。

Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64

4peL4peL5qeYCuOBhOOBpOOCguOBiuS4luipseOBq+OBquOBo+OBpuOBiuOCiuOBvuOBmeOAgg==

添付ファイルに関しては、ファイルのタイプと変換方式を指定し、添付したデータのファイル名等の情報は Content-Disposition ヘッダーフィールドに含みます。

Content-Type: image/jpeg
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=“attachment.jpeg"

(省略)

マルチパートメッセージの記述が終わったら、その内容をURLエンコードした後にリクエスト本文のRawMessage.Dataパラメータに設定して、メールを送信してみてください。 例として以下のようなメールデータを送ることにします。

From: sender@example.com
To: receiver@example.com
Subject: =?utf-8?B?44OG44K544OI44Oh44O844Or?=
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=boundary_str

--boundary_str
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64

4peL4peL5qeYCuOBhOOBpOOCguOBiuS4luipseOBq+OBquOBo+OBpuOBiuOCiuOBvuOBmeOAgg==

--boundary_str
Content-Type: image/jpeg
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="nifcloud-icon.jpeg"

/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxAQDxASDw8QDxANEA8QERYPEBAQEg8VFRYWFhURFR
gYHSgsGBolGxUVITEiJiorLi4vFx8zOjMtNygtLisBCgoKDg0OGxAQGy4mHyU1LTYtLi8rMC4tLy8t
...

--boundary_str

メールアプリで確認すると以下のように添付ファイルの送信ができました。

f:id:TuanNguyen:20191218140116j:plain
図3. 添付ファイル送信結果

5 まとめ

ニフクラESSのAPIを利用したメール送信・配信ログ取得方法を紹介しました。他にも色々なAPIを提供していますので、手軽に試していただければと思います。(参考リンク) 本チュートリアルの内容をご覧いただき、ニフクラESS APIでメール配信の操作を行う際の参考にしていただけると幸いです。

6 注意事項

ニフクラESS APIを利用する際の制限は以下となります。ご確認お願いします。

  • 50宛先/1リクエスト(SMTPインターフェイスの方が配信数の上限数が多く、 大規模な送信にはSMTPインターフェイスの利用を検討してください)
  • 1リクエスト/0.1秒(0.1秒以内に2リクエスト以上実行しようとした場合は一時的なエラーを返却します)

7 付録

7.1 コード

import urllib.parse
import datetime
import hashlib
import hmac
import requests

class EssApiRequest():
  def __init__(self):
    # ESS APIの共通情報
    self.api_endpoint = "https://ess.api.nifcloud.com"
    self.host = "ess.api.nifcloud.com"
    self.region = "east-1"
    self.service = "email"
    self.access_key = *******************
    self.secret_access_key = **********************

  def sendEmail(self):
    # SendEmail APIのパラメータの準備
    request_method = "POST"
    source_address = urllib.parse.quote("sender@example.com")
    destination_address = urllib.parse.quote("receiver@example.com")
    email_subject = urllib.parse.quote("テストメール")          # URLエンコード
    email_body = urllib.parse.quote("メール送信が成功しました")
    request_payload = f"Action=SendEmail&Source={source_address}&Destination.ToAddresses.member.1={destination_address}&Message.Subject.Data={email_subject}&Message.Body.Text.Data={email_body}&Version=2010-12-01"
    headers = self.headerPrepare(request_method, request_payload)
    self.apiRequest(request_payload, headers)

  def headerPrepare(self, request_method, request_payload):
    t = datetime.datetime.utcnow()
    nifty_date = t.strftime('%Y%m%dT%H%M%SZ')
    datestamp = t.strftime('%Y%m%d')

    canonical_uri = '/'
    canonical_querystring = ""

    canonical_headers = 'host:' + self.host + '\n' + 'x-nifty-date:' + nifty_date + '\n'

    signed_headers = 'host;x-nifty-date'

    request_payload_hash = hashlib.sha256(request_payload.encode('utf-8')).hexdigest()

    canonical_request = request_method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + \
        canonical_headers + '\n' + signed_headers + '\n' + request_payload_hash
    algorithm = 'NIFTY4-HMAC-SHA256'
    credential_scope = datestamp + '/' + self.region + '/' + self.service + '/' + 'nifty4_request'
    string_to_sign = algorithm + '\n' +  nifty_date + '\n' +  credential_scope + '\n' +  \
        hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()

    def sign(key, msg):
        return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
    kDate = sign(("NIFTY4" + self.secret_access_key).encode('utf-8'), datestamp)
    kDateRegion = sign(kDate, self.region)
    kDateRegionService = sign(kDateRegion, self.service)
    kSigning = sign(kDateRegionService, "nifty4_request")
    signature = hmac.new(kSigning, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest()

    authorization_headers = algorithm + ' ' + 'Credential=' + self.access_key + '/' + \
        credential_scope + ', ' +  'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature

    headers = {'x-nifty-date':nifty_date, 'Authorization':authorization_headers, 'Content-type':'application/x-www-form-urlencoded'}
    return headers

  def apiRequest(self, request_payload, headers):
    # リクエスト送信
    r = requests.post(url=self.api_endpoint, data=request_payload, headers=headers)
    print('\nRESPONSE++++++++++++++++++++++++++++++++++++')
    print(f"Response code: {r.status_code}\n")
    print(f"Response Body: \n{r.text}")

if __name__=="__main__":
  essApiRequest = EssApiRequest()
  essApiRequest.sendEmail()
  

7.2 シグネチャーバージョン4の計算について

もう少し説明するとシグネチャーバージョン4はユーザーの認証情報とリクエストの内容を暗号化アルゴリズムで署名用の文字列(シグネチャーと呼ぶ)を生成し、シグネチャーをリクエストに組み込むことによって署名プロセスを行う方法です。リクエストを受け取ったニフクラ側も同様の過程でリクエストの内容から、ユーザーの認証情報を用いて、シグネチャーを計算し、リクエストに組み込んだシグネチャーと同じなものかどうかを確認します。もしシグネチャーが一致しない場合、リクエストを拒否します。そのため、リクエストの改ざんなどを防ぐことができ、セキュリティーが向上します。

7.2.1 署名用文字列の作成

署名用文字列は、アルゴリズム・日付・証明範囲・ハッシュ化された正規化リクエストを、以下のように改行で結合した文字列です。

StringToSign = "NIFTY4-HMAC-SHA256" + "\n" +
                 RequestDate + "\n" +
                 CredentialScope + "\n" +
                 Hex(SHA256Hash(CanonicalRequest))

それぞれの要素は以下に説明します。

a) RequestDate
  • リクエストの日付・時刻で ISO8601に従う記述(例:20190101T000000Z
  • 例として以下の Pythonコードで用意することができます。
# リクエストの日付・時刻の文字列の作成の例
import datetime
t = datetime.datetime.utcnow()
nifty_date = t.strftime("%Y%m%dT%H%M%SZ") # 例:20190101T000000Z
b) CredentialScope
  • 署名範囲であり、次のような文字列になります。
    • 【日付(YYMMDD)】/【ニフクラリージョン】/【ニフクラサービス識別子】/nifty4_request
# 署名範囲の文字列作成の例
import datetime
t = datetime.datetime.utcnow()
datestamp = t.strftime('%Y%m%d') # 例: 20190101
region = "east-1"
service = "email"
credential_scope = datestamp + "/" + region + "/" + service + "/" + "nifty4_request"
c) CanonicalRequest(正規化リクエスト)

リクエストの要素から組み合わせたものです。Canonical Request は以下のような組み合わせで作成されます。

CanonicalRequest = HTTPRequestMethod + '\n' +
                   CanonicalURI + '\n' +
                   CanonicalQueryString + '\n' +
                   CanonicalHeaders + '\n' +
                   SignedHeaders + '\n' +
                   HexEncode(Hash(RequestPayload))
  • HTTPRequestMethod : "GET|PUT|POST"など
# リクエストの例
request_method = "POST"
  • CanonicalURI : 絶対パスをURLエンコードしたものを指定します。(URLのドメイン部分から「?」マークまでの間の部分を指定します。)
# 正規化URIの例
canonical_url = "/"
  • CanonicalQueryString : クエリストリングを正規化したものを指定します。 リクエストがクエリストリングを含まない場合は、empty string(長さ0の文字列)です。正規化クエリストリングを構成するためには、以下の手順を実施します。
    • ステップ1:パラメーター名と値をURLエンコードします。
    • ステップ2:パラメーター名でASCIIコード順にソートします。
    • ステップ3:ソートリストの先頭のパラメータ名から順に正規化クエリストリングに追加する作業を行います。それぞれのパラメーター名とパラメーター値を'='で結合し、パラメーターを「&」で結合します。ただし最後のパラメーターには不要です。
# ニフクラESS APIの場合はempty string(長さ0の文字列)です。("")
canonical_query_string = ""
  • CanonicalHeaders: ヘッダフィールドには、hostやx-nifty-dateを含めます。
# 正規化ヘッダ作成の例
canonical_headers = "host:" + api_endpoint + "\n" +
                   "x-nifty-date:" + nifty_date + "\n"
  • SignedHeaders: ヘッダパラメーターのパラメーター名のリストを指定します。
signed_headers = "host;x-nifty-date"
  • RequestPayload: HTTPリクエストのボディ部の内容です(ここではペイロードと呼びます)。また、HexEncode(Hash()) はペイロードをハッシュ化し、16進数エンコードしたものを意味します。
# リクエストのペイロード作成の例
import hashlib
request_payload = f"Action={api_name}&Version=2010-12-01&Source={source_address}&Destination.ToAddresses.member.1={destination_address}&Message.Subject.Data={email_subject}&Message.Body.Text.Data={email_body}"
request_payload_hash = hashlib.sha256(request_payload.encode('utf-8')).hexdigest()
  • CanonicalRequest(正規化リクエスト)は上記で計算した要素から作成することができます。
# 正規化リクエストの計算の例
canonical_request = request_method + '\n' + \
                    canonical_uri + '\n' + \
                    canonical_query_string + '\n' + \
                    canonical_headers + '\n' + \
                    signed_headers + '\n' + \
                    request_payload_hash

最後に、作成した文字列から署名用文字列を作成することができます。

# 署名用文字列作成の例
import hashlib, hmac
string_to_sign = "NIFTY4-HMAC-SHA256" + '\n' + \
                nifty_date + '\n' +  \
                credential_scope + '\n' +  \
                hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()

7.2.2 署名用キーの作成

署名用キーはリクエストのシークレットアクセスキー・日付・ニフクラのリージョン・サービス識別子から計算されたものです。 以下の手順で作成されます。

DateKey              = HMAC-SHA256(“NIFTY4” + “【 シークレットアクセスキー 】 “ ,  “【 YYYYMMDD 】”)
DateRegionKey        = HMAC-SHA256(DateKey , “【 ニフクラリージョン 】”)
DateRegionServiceKey = HMAC-SHA256(DateRegionKey , “【 ニフクラサービス識別子】”)
SigningKey           = HMAC-SHA256(DateRegionServiceKey , “nifty4_request”)

ニフクラESSの場合は以下となります。シークレットアクセスキーは、仮に 1234567890abcdefghijklmnopqrstuvwxyzABCD とします。

# キー計算の例
import hashlib, hmac
def sign(key, msg):
    return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
secret_access_key = "1234567890abcdefghijklmnopqrstuvwxyzABCD"
kDate = sign(("Nifty4" + secret_access_key).encode('utf-8'), datestamp)
kDateRegion = sign(kDate, region)
kDateRegionService = sign(kDateRegion, service)
kSigning = sign(kDateRegionService, "nifty4_request")

7.2.3 シグネチャーの計算

最後にシグネチャーは①と②で作成された StringToSign(署名用文字列) と SigningKey(署名用キー) から図1に示した式によって計算されます。

# シグネチャー計算の例
import hashlib, hmac
signature = hmac.new(kSigning, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest()