0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

twilio SendGrid Inbound Parse Webhookからbrevoに移行してみた。

Last updated at Posted at 2025-07-21

Motivation

twilioとSendGridとherokuでキャリアメール代替プッシュ通知twilioからvonageに移行してみたで、SendGrid(現twilio)のInbound Parse Webhookを使い、子供の習い事の終了メールを受けると、その内容をテキスト読み上げし電話で受けてたのですが、twilio SendGridからFree Email APIを2025/7/26に終了するという最後通告が来たので、移行先を探しbrevoに移行してみました。
これこれと似たような話です。。。

brevoのアカウントをとる。

細かい手順忘れましたが、PricingのFreeプランになるよう、アカウントを作りました。
free.jpg

API Key取得

後述webhookエンドポイント登録で必要になるので、Transactional > Settings > Configuration > HTTP APIGet your API keyからAPI Keyを生成しておきます。
api.jpg

DNS登録

twilioとSendGridとherokuでキャリアメール代替プッシュ通知DNS登録同様、AWS Route53で運用しているドメイン宛のメールをbrevoで受けられるようbrevo側とRoute53側へDNS設定します。
domain.jpg

Your Senders&DomainsDomainsからAdd a domainしてメールを受けるドメインを登録します。登録が終わると下記のようになります。
domain2.jpg
domain3.jpg

Type TXTとCNAMEのDNSレコードが2つずつ(計4レコード)表示されるのでRoute53に登録します。加えて、メールを受けるためにMXレコードが必要なので、Inbound parsing webhooksに記載のとおりのMXレコードも登録します。やり方が悪いのかRoute53で一つしかMXレコードを登録できなかったのでPriority 10の方だけ登録しました(とりあえずメール受けれてます)。
dns.jpg

webhookエンドポイント登録

Inbound parsing webhooksCreating 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 webhooksParsed 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登録したドメインでないと送信できないようです。
dify.jpg

curl.
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)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?