TonamiLog

ゆるキャン△とスノーボードとオタク

夏休み自由工作:通販したら着弾日がTimeTreeに登録されるやつ②


ハロートナミです。引き続き

Amazonから届く確認メールをあれこれして、TimeTreeに着弾時間を登録するやつ

を作っていきます。
これは続きなので、前のやつを読みたい方はこれを読んで下さい。


前回の記事でTimeTreeにAPIを投げて予定を登録する処理を確認したので、残りのやることが

  • Amazonからの発送通知メールをいい感じに取ってくる実装
  • Lambdaに乗せる
  • 動かしてみる

という感じになっていました。それではやっていきましょう。


Amazonからの発送通知メールをそれがし

元々Amazonのアカウントはyahoomailで作っていたのですが、これを期にGmailに変更して
IFTTTとかそのへんの仕組みを使ってシュッと作っていきます。

……と思ってたんですが、GmailのIFTTT連携がセキュリティ上の理由で消滅していました。
かなりガッカリしました。

調べるとGoogle Apps Script(GAS)で頑張って上記内容を実現してる人もいたんですが
今回は別にリアルタイムに処理されなくても良いし、管理する対象が増えるのが嫌なので
メールサーバからメールを取得してメキメキ処理するオールドスクールな方法でいきます。

メールの取得はやった事無いのでインターネットで何とかします。
qiita.com特にこの記事を参考にしました。

最終的に生まれたコードがこちらです。

import sys
import imaplib
import email
from email.header import decode_header, make_header
import datetime
import re

class MailServer():
    IMAP_cli = None

    def __init__(self, server):
        self.IMAP_cli = imaplib.IMAP4_SSL(server)

    def login(self, user, password):
        self.IMAP_cli.login(user, password)
    
    def logout(self):
        self.IMAP_cli.close()
        self.IMAP_cli.logout()

    def _decode_message(self, msg):
        # メール本文のデコード
        if msg.is_multipart() is False:
            # シングルパートのとき
            payload = msg.get_payload(decode=True)
            charset = msg.get_content_charset()
            if charset is not None:
                message = payload.decode(charset, "ignore")
        else:
            # マルチパートのとき
            for part in msg.walk():
                payload = part.get_payload(decode=True)
                if payload is None:
                    continue
                charset = part.get_content_charset()
                if charset is not None:
                    message = payload.decode(charset, "ignore")
        
        return message

    def get_amazon_arriving_estimates(self):
        self.IMAP_cli.select('amazon')

        # まだ確認していない、件名に発送が入っているアマゾンからのメールを抽出
        typ, data = self.IMAP_cli.search(None, f'(SUBJECT "item has shipped" FROM "Amazon.co.jp" NEW)')
        estimates = []
        for num in data[0].split():
            typ, data = self.IMAP_cli.fetch(num, '(RFC822)')
            email_message = email.message_from_bytes(data[0][1])
            message = self._decode_message(email_message)

            # 1通のメールに複数配達日が書いてあるケースを考慮し、配達日毎のまとまりに分割
            arrivings = message.split('Arriving')[1:] # 0個目はヘッダ等なので省く
            for arriving in arrivings:
                description = ''
                # 配達予定の商品を抽出する
                # 商品名 - 販売元……の順で記載されているので、Sold byで分割し、各要素の最後の部分を使う
                solds = arriving.split('Sold by:')[:-1]
                for sold_by in solds:
                    if description != '':
                        description += '\n'
                    # 各まとまりを後ろから商品名周辺のタグで検索し、スライスで商品名を抽出
                    description += sold_by[sold_by.rfind('sans-serif">') + 13:sold_by.rfind('</a>')]

                # 商品名の記載が無ければ追加しない
                if description == '':
                    continue

                # 配達予定日を抽出。大体ここに書いてあるというアタリで取り出す
                estimate_date = re.search(r'\d{2}\/\d{2}', arriving[:50]).group()
                estimates.append((estimate_date, description))

        return estimates

なげー

メールの取得までは素直だと思うんですが
メールの文字列を気合パースして配達予定日と配達内容を取得してるところがカオスで
自分でもメンテナンスしたくない感じのコードになりました。
コードだけじゃ絶対分からないのでコメントが沢山書いてあります。酷いですね

何はともあれ、これを以下の感じで使うと、配達予定日と商品のセットが取得出来るはずです。

import os

ms = MailServer(os.environ['MAIL_SERVER'])
ms.login(os.environ['MAIL_SERVER_USER'], os.environ['MAIL_SERVER_PASS'])

estimates = ms.get_amazon_arriving_estimates()
for estimate_date, description in estimates:
    print(estimate_date)
    print(description)

# 08/06
# CANARE XLRケーブル マイクケーブル ノイトリックコネクター 黒色 1.5m EC015-B/黒 
# 【国内正規品】RODE ロード PodMic ポッドキャスト向けダイナミックマイク PODMIC
# 08/07
# Rode PSA1 プロフェッショナル・スタジオ・ブームアーム [並行輸入品] 
# 08/06
# サンワダイレクト チェアマット 半透明タイプ 傷防止 フローリング 畳 Pタイル 対応 100-MAT002

メールの仕様が変わったら?その時はおしまいです。


やってて気付いたんですが、amazonの配達お知らせメールって時間まで書いてないんです。
メールからは日付しか取れないし、なんなら時間を取る手段って良く分からない(配送会社に依存する?)ので
とりあえず諦めて、日付だけ指定の終日予定として登録する事に変更しました。


色々迷走しましたが、何とかAmazonの配達予定日とかを取得する部分が出来ました。
先日作ったTimeTreeに予定を登録するやつと組み合わせる用に、形式を変換する処理とかを適当に書いて……

estimates = ms.get_amazon_arriving_estimates()

for estimate_date, description in estimates:
    yaer = datetime.datetime.now().year
    if datetime.datetime.now().month == 12 and '01/' in estimate_date:
        # 12月中に届いた1月のお届予定は来年1月として処理
        yaer += 1
    estimate_date = datetime.datetime(year=yaer, month=int(estimate_date[0:2]), day=int(estimate_date[3:5]))

    tt.add_schedule('amazon来る', description, estimate_date)

メールの検索ルールをちょっと弄って、古い通知メールで動作確認です。

---
f:id:Thiroyuki:20200810052518p:plain
---
この注文が

---
f:id:Thiroyuki:20200810052445p:plain
---
こう。いい感じです。

何とかamazonの配達通知をTimeTreeの予定に追加する事が出来ました。


残るはLambdaに搭載する作業と、適当に実行ルール作って動かしてみる作業ですが
コード貼った関係で記事が長くなったので、続きは次の記事に持ち越そうと思います。
次の記事で終わりにしたいですね。技術っぽい話は書くの大変な割に誰も求めてないですからね。

あ、手元のコードはもうちょっと整理したらGithubに投げてリンクしておきます。
それでは次のエントリでお会いしましょう。バイ