薬剤師のプログラミング学習日記

プログラミングやコンピュータに関する記事を書いていきます

OutlookメールをPythonでファイル出力する

Outlookで受信したメールを特定のフォルダにHTMLファイルとして出力するプログラムです。私の職場では医薬品医療機器情報配信サービス(PMDAメディナビ)を利用していますが、このメールをネットワーク内の共有フォルダにHTML形式で保存することで、ネットワーク内のパソコンからリンク先に飛ぶことができるようになります。
f:id:enokisaute:20200418165535p:plain
読んでほしいところは転送すればいいだけなのですが、メールのカテゴリ毎(【重要】、使用上の注意の改訂、取り違え注意、など)のフォルダに保存し、また、添付ファイルがあった場合はそれも保存できるように自動化してみました。

なお環境はAnaconda(Python3.7)、Windows10のOutlook2019で動作確認しました。

パッケージの確認と下準備

PythonからOutlookを操作するにはpywin32というパッケージがあるかどうかを確認します。
Anaconda Promptを開いて(左下の検索窓でAnaconda Promptと入力)、

conda list pywin32

と入力してみます。Anacondaをインストールした人は最初から入っていますが、なければインストールする必要があります。Anacondaのインストールについてはこちらに記事を書きました。
www.yakupro.info


次はOutlookの設定です。Outlookで受信した全メールを対象にプログラムで毎回処理するのは無駄が多いので、先に受信トレイにフォルダを作っておきます。プログラムはそのフォルダ内のメールだけを処理の対象とするようにします。

Outlook受信トレイ上で右クリック→新しいフォルダを作成を選びます。フォルダ名は「PMDAメール」としました。名前は適宜変更してください。
また、処理したいメールがこのフォルダに入るように、メールを右クリック→ルールを作成→『差出人が次の場合』のところは「PMDAメディナビ」、『実行する処理』は「アイテムをフォルダに移動する」を選択しました。
あと、PMDAメディナビでこのプログラムを動かす場合は『配信メールの形式』を「HTML形式」にしておきましょう。<登録内容の変更>から設定できます。

Outlookメールをコンソールに表示する

最小限のコードで受信メールを表示してみます。

import win32com.client
 
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
inbox = outlook.GetDefaultFolder(6)
folders = inbox.Folders
pmdaFolder = folders('PMDAメール')    # 作成したoutlookのフォルダ名
messages = pmdaFolder.Items
 
for message in messages:
    print('Subject: ' + message.subject)
    print('Sender: ' + message.sendername)
    print('ReceivedTime: ' + str(message.receivedtime))
    print(message.Body)    # message.HTMLBodyならHTML形式で表示
    print()

これを実行するとメールの件名(Subject)、送信者(Sender)、受信日時(RecievedTime)、本文が表示されます。

プログラムのざっくりした説明

プログラムではメールを一件ずつみていき、メール本文にURLがあった場合は<a href="リンク先URL"></a>のような形に直して拡張子「.html」で保存していきます。

また、決め事として、次のようなものを設定しました。

  • 「受信日時(yyyy-mm-dd)+メールの件名」を保存ファイル名とする。
  • 特定の期日から手前(現在まで)のメールについてのみ処理を行う。

ループ内の処理の前半はこのようなファイル名を生成したり、処理対象の期間のメールであるかの判定、出力先パスの生成などです。
保存先フォルダのイメージは次の図のようになります。
f:id:enokisaute:20200424010700p:plain
プログラムを実行すると、『DSU』フォルダには次のようなファイルが作成されます。
f:id:enokisaute:20200424002703p:plain
『2020-03-10医薬品安全対策情報(DSU)掲載のお知らせ.html』をダブルクリックすると、ブラウザで開くことができリンク先を参照することができます。(画像は一部加工)
f:id:enokisaute:20200424001950p:plain

正規表現によるURL抽出

このプログラムで一番肝になる部分です。
正規表現とは、これまた大雑把な説明をすると、特定のパターンを持つ様々な文字列を簡潔に表現しよう、ということです。検索や抽出、置換などで用いられます。

# 先にコンパイルしておく
url_pattern = re.compile(r'http[s]?://.*(?:.html|.pdf|.jp[^/]|-\d{4}|iyakuSearch/|[0-9A-Z]{10, 17})')

URLなので「http(s)://」から始まります。途中いくつか文字があって、末尾は

  • 「.html」または「.pdf」
  • 「-数字4桁」
  • 「iyakuSearch/」
  • 「数字か英字大文字の10~17桁」

で抽出することにしました。(https://www.pmda.go.jpの場合だけちょっと処理を変えていますが、詳細は割愛)
これをurl_patternとして、re.sub()を使って<a href="URL"></a>に置換しています。
PMDAメディナビの2019年1月~2020年4月までのメールで試したところ、これでほぼ全てのURLを抜き出すことができました。

参考

  • 退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング

ピンポイントな要件を処理するプログラムですが、サンプルコードとして今回のコードを載せておきます。

import win32com.client
import re
import os
import datetime
 
# 先にコンパイルしておく
url_pattern = re.compile(r'http[s]?://.*(?:.html|.pdf|.jp[^/]|-\d{4}|iyakuSearch/|[0-9A-Z]{10, 17})')
 
# subに渡す関数. 引数matchは抽出されたパターン
def create_link(match):
    m = match.group()
    if re.search(r'jp.$', m):
        # https://www.pmda.go.jpの場合
        url_string = '<a href="' + m[:-1] + '">' + m[:-1] + '</a>' + m[-1]
    else:
        url_string = '<a href="' + m + '">' + m + '</a>'
    return url_string
 
# URLをリンクとしてクリックできるよう置換する
def replace_url(text):
    return url_pattern.sub(create_link, text)
 
# ファイル名を日付に変換して渡された日付(y, m, d)以降(True)か以前(False)か判定する
def check_date(file_name, y, m, d):
    date_list = file_name.split('-')
    year = int(date_list[0])
    month = int(date_list[1])
    day = int(date_list[2][:2])
    if datetime.date(year, month, day) > datetime.date(y, m, d):
        return True
    else:
        return False
 
# ファイル名のキーワードをみて, 保存先フォルダを決める
def get_save_path(file_name, base_path):
        for folder in folder_list:
            if folder in file_name:
                return base_path + folder + '\\'
        return base_path + OTHER + '\\'
 
 
if __name__ == '__main__':
    outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
    inbox = outlook.GetDefaultFolder(6)
    folders = inbox.Folders
    pmdaFolder = folders('PMDAメール')  # outlookのフォルダ名
    messages = pmdaFolder.Items
 
    DAYS = 90        # DAYS日前まで遡って処理する
 
    # 保存先のベースとなるパス. 共有フォルダ等の保存したい場所を設定する
    BASE_PATH = 'C:\\Users\\****\\Desktop\\メール\\'
 
    # メール保存先フォルダのリスト(メールのSubjectで使われているキーワードをフォルダ名にする)
    folder_list = ["【重要】", "使用上の注意の改訂", "取り違え注意", "DSU"]
    OTHER = "その他"
    ATTACHED_FILE_FOLDER = "添付ファイル\\"
 
    counter = 0     # メールを何通処理(変換)したか
 
    for message in messages:
        # ファイル名に使えない文字はあらかじめ取り除いておく
        msg_subject = re.sub(r'¥|/|:|\*|\?|"|<|>|\||', '', message.subject)
        # 受信日時(yyyy-mm-dd)とメールサブジェクトを合わせたものをファイル名とする
        file_name = str(message.receivedtime).split(' ')[0] + msg_subject
        # 今日からDAYS日前の日付を処理の期限とする
        period = datetime.date.today() - datetime.timedelta(days=DAYS)
 
        # periodより以降(period~今現在)のメールについてのみ処理を行う
        if check_date(file_name, period.year, period.month, period.day):
            # 出力先パスの作成
            output_path = get_save_path(file_name, BASE_PATH)
            output_file = output_path + file_name + '.html'
 
            # 既にファイルが存在する可能性があるので先にチェックする. あれば次のメールへ処理を移す
            if os.path.exists(output_file):
                continue
            print('Subject: ' + message.subject)
            print('Sender: ' + message.sendername)
            print('ReceivedTime: ' + str(message.receivedtime))
            print()
            counter += 1
 
            # URLの置換処理. 
            html_text = replace_url(message.HTMLBody)
 
            with open(output_file, mode='w') as f:
                f.write(html_text)
 
            # 添付ファイルがあれば保存
            if message.attachments.count is not 0:
                for attachment in message.attachments:
                    attachment.SaveASFile(BASE_PATH +
                        ATTACHED_FILE_FOLDER + str(attachment))
 
    print()
    print(">>" + str(counter) + "通のメールを処理しました。")
    print('>>Enterを押して終了')
    input()