Motivation
twilioとSendGridとherokuでキャリアメール代替プッシュ通知、twilioからvonageに移行してみたで、SendGrid(現twilio)のInbound Parse Webhookを使い、子供の習い事の終了メールを受けると、その内容をテキスト読み上げし電話で受けてたのですが、twilio SendGridからFree Email APIを2025/7/26に終了するという最後通告が来たので、移行先を探しbrevoに移行してみました。
これやこれと似たような話です。。。
brevoのアカウントをとる。
細かい手順忘れましたが、PricingのFreeプランになるよう、アカウントを作りました。
API Key取得
後述webhookエンドポイント登録で必要になるので、Transactional > Settings > Configuration > HTTP APIのGet your API keyからAPI Keyを生成しておきます。
DNS登録
twilioとSendGridとherokuでキャリアメール代替プッシュ通知のDNS登録同様、AWS Route53で運用しているドメイン宛のメールをbrevoで受けられるようbrevo側とRoute53側へDNS設定します。
Your Senders&DomainsのDomainsからAdd a domainしてメールを受けるドメインを登録します。登録が終わると下記のようになります。
Type TXTとCNAMEのDNSレコードが2つずつ(計4レコード)表示されるのでRoute53に登録します。加えて、メールを受けるためにMXレコードが必要なので、Inbound parsing webhooksに記載のとおりのMXレコードも登録します。やり方が悪いのかRoute53で一つしかMXレコードを登録できなかったのでPriority 10
の方だけ登録しました(とりあえずメール受けれてます)。
webhookエンドポイント登録
Inbound parsing webhooksのCreating the webhookの通り、上記で生成したAPI Keyを用いてcurlでwebhookのエンドポイントを登録します。上記Your Senders&Domainsで設定したドメインでないと登録できないようです。
curl --request POST \
--url https://api.brevo.com/v3/webhooks \
--header 'accept: application/json' \
--header 'api-key: your_api_key' \
--header 'content-type: application/json' \
--data '
{
"type": "inbound",
"events": ["inboundEmailProcessed"],
"url":"https://your_webhook_endpoint.com/xxxx",
"domain": "mail.your_domain.de",
"description":"Webhook to receive replies"
}
'
webhookエンドポイントのコード修正
Inbound parsing webhooksのParsed email payload通りに、受信したメールがJSON bodyとしてPOSTされてくるのですが、twilioとSendGridとherokuでキャリアメール代替プッシュ通知のheroku上にWebアプリ構築のSendGrid仕様とは当然違うので修正します。
handle_email()
関数の中で
envelope = simplejson.loads(request.form['envelope'])
のように概ねフラットなJSONのkey/valueを都度dictに取り込んでいたのを、下記な感じで一度に取り込んでます。
item = simplejson.loads(request.get_data())['items'][0]
参考までにオリジナルのコードとのdiffを末尾につけておきます。
SendGridの時はメールのcharsetがiso-2022-jpの場合はtwilioからvonageに移行してみたのtwilioからのマイグレ > コードのようにデコード処理してたのですが、brevoはcharsetによらずutf-8でエンコードしてくるようです。
番外: POSTでメールを送る。
別件でDifyで定期的にあれこれ処理した結果を、HTTP RequestでSendGridのメール送信API(V3 Mail Send API)を使ってメール送信してたので、それもEMAIL | Send a transactional message(下記curl)を参考に変更しました。senderのドメインは上記DNS登録したドメインでないと送信できないようです。
curl --request POST \
--url https://api.brevo.com/v3/smtp/email \
--header 'accept: application/json' \
--header 'api-key: your_api_key' \
--header 'content-type: application/json' \
--data '{
"sender":{
"name":"Dify",
"email":"sender_name@mail.your_domain.de"
},
"to":[
{
"email":"receiver_name@receiver_domain.com",
"name":"your receiver"
}
],
"subject":"hoge",
"textContent":"fuga"
}'
最後に
brevoもこれやこれ、本記事のMotivationのように、ならないことを祈ります。
参考
@@ -1,15 +1,16 @@
import logging
import os
-import ConfigParser
+import configparser
from flask import Flask
from flask import request
from flask import url_for
-from twilio.rest import TwilioRestClient
import phonenumbers as ph
import sendgrid
import simplejson
+import vonage
+
from konfig import Konfig
@@ -20,7 +21,7 @@
address_book = {}
address_book_file = 'address-book.cfg'
try:
- user_list = ConfigParser.ConfigParser()
+ user_list = configparser.ConfigParser()
user_list.read(address_book_file)
for user in user_list.items('users'):
address_book[user[0]] = user[1]
@@ -30,10 +31,13 @@
app = Flask(__name__)
konf = Konfig()
-twilio_api = TwilioRestClient()
sendgrid_api = sendgrid.SendGridClient(konf.sendgrid_username,
konf.sendgrid_password)
-
+vonage_client = vonage.Client(key=konf.vonage_api_key,
+ secret=konf.vonage_api_secret,
+ application_id=konf.vonage_application_id,
+ private_key="/etc/secrets/VONAGE_APPLICATION_PRIVATE_KEY")
+vonage_voice = vonage.Voice(vonage_client)
class InvalidInput(Exception):
def __init__(self, invalid_input):
@@ -89,7 +93,7 @@
try:
number = ph.parse(potential_number, 'US')
phone_number = ph.format_number(number, ph.PhoneNumberFormat.E164)
- except Exception, e:
+ except Exception as e:
raise InvalidPhoneNumber(str(e))
if phone_number in self.by_phone_number:
@@ -104,7 +108,7 @@
try:
number = ph.parse(potential_number, 'US')
phone_number = ph.format_number(number, ph.PhoneNumberFormat.E164)
- except Exception, e:
+ except Exception as e:
raise InvalidPhoneNumber(str(e))
phone_number = phone_number.replace('+', '')
return("{}@{}".format(phone_number, konf.email_domain))
@@ -125,9 +129,7 @@
def check_for_missing_settings():
rv = []
- for required in ['EMAIL_DOMAIN',
- 'SENDGRID_USERNAME', 'SENDGRID_PASSWORD',
- 'TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN']:
+ for required in ['VONAGE_API_KEY', 'VONAGE_API_SECRET']:
value = getattr(konf, required)
if not value:
rv.append(required)
@@ -151,7 +153,7 @@
error_message = template.format(missing)
return warn(error_message), 500
elif duplicates_in_address_book():
- print str(address_book)
+ print(str(address_book))
error_message = ("Only one email address can be configured per "
"phone number. Please update the 'address-book.cfg' "
"file so that each phone number "
@@ -182,7 +184,7 @@
'from_email': phone_to_email(request.form['From']),
'to': lookup.email_for_phone(request.form['To'])
}
- except InvalidInput, e:
+ except InvalidInput as e:
return warn(str(e)), 400
message = sendgrid.Mail(**email)
@@ -200,23 +202,46 @@
def handle_email():
lookup = Lookup()
try:
- envelope = simplejson.loads(request.form['envelope'])
- lines = request.form['text'].splitlines(True)
- sms = {
- 'to': email_to_phone(request.form['to']),
- 'from_': lookup.phone_for_email(envelope['from']),
- 'body': lines[0]
- }
- except InvalidInput, e:
+ item = simplejson.loads(request.get_data())['items'][0]
+ warn(str(item))
+ form_body = None
+ if 'RawTextBody' in item:
+ form_body = 'RawTextBody'
+ elif 'RawHtmlBody' in item:
+ form_body = 'RawHtmlBody'
+ lines = item[form_body].splitlines(False)
+ lines[0].replace("\r\n", " ")
+ line_num = 1
+ while line_num < len(lines):
+ lines[0] +=lines[line_num].replace("\r\n", " ")
+ line_num += 1
+
+ email_to = item['Recipients'][0]
+ if email_to != item['To'][0]['Address']:
+ email_from = 'sender_name@my_sender.com'
+ else:
+ email_from = item['From']['Address']
+ except InvalidInput as e:
return warn(str(e))
try:
- rv = twilio_api.messages.create(**sms)
- return rv.sid
+ responseData = vonage_voice.create_call({
+ 'from': {'type': 'phone', 'number': lookup.phone_for_email(email_from).replace('+','')},
+ 'to': [{'type': 'phone', 'number': email_to_phone(email_to).replace('+','')}],
+ 'ncco': [
+ {
+ "action": "talk",
+ "text": lines[0][:255],
+ "language": "ja-JP",
+ "style": 2
+ }
+ ]
+ })
+ return responseData["status"]
except Exception as e:
- print "oh no"
- print str(e)
- error_message = "Error sending message to Twilio"
+ print("oh no")
+ print(str(e))
+ error_message = "Error sending message to Vonage"
return warn(error_message), 400
if __name__ == "__main__":
@@ -224,5 +249,5 @@
port = int(os.environ.get('PORT', 5000))
if port == 5000:
app.debug = True
- print "in debug mode"
+ print("in debug mode")
app.run(host='0.0.0.0', port=port)