【Python】JSONファイルのデータを元にOutlook新規メールを作成するツールを作成したことと、引っ掛かった部分について

#1.この記事について
主目的は、
日次メール作成作業や月次定期メールなど
定型的な作業を効率化したかったこと。

第二の目的は、
Pythonにおけるjsonファイルの扱いや
Outlookの連携を学習したかった。
（率直なところOutlookをいじるだけなら
Excelにデータを載せてVBAで作成するのが
一番正着なんではないかと思うけど）

######[※ツールとソースコードはGithubに上げてます（後述）](#5終わりに)

#2.やりたいこと
・jsonファイルの項目を考える
・jsonファイルをハンドリングするクラス作成
・Office Outlookをハンドリングするクラス作成
・上記クラスを連携してメール作成するmainメソッドの開発

#3.使用したツール・環境
・Windows10
・Python 3.7.0
・office2016
・その他下記コードに記載のライブラリ各種

#4.作成したコードと解説

##クラス図
pycharmで作成したクラス図はこのようになる。
![OutlookMailCreater.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541790/8a959383-c421-bf70-5838-2e59bb3072ce.png)

##jsonファイルの中身

要件としては、
・フォーマット（テキストメールかHTMLメールかリッチテキストか）
・差出人
・送り先（複数指定可）
・CC（複数指定可）
・BCC（複数指定可）
・件名
・本文
・添付ファイル
を設定できるものが望ましかったため、
このようになった。

```json:mailSetting.json
{
  "mailFormat": 3,
  "senderAddress": "foo@gmail.com",
  "receiverAddressSet": ["bar@Test.jp","bar2@Test.jp","bar3@Test.jp"],
  "CC_AddressSet": ["bar@Test.jp","bar4@Test.jp"],
  "BCC_AddressSet": ["bar@Test.jp","bar5@Test.jp"],
  "title":"あいうえお",
  "attachedFileFolderPath": "C:\\Users\\UserName\\Test",
  "body": "〇〇様\n\nお疲れさまです。〇〇です。\n\n〇〇を送付します。\n\n以上。\n"
}

```

mailFormatは1~3の数字から選び、
それぞれが[テキストメールかHTMLメールかリッチテキストか]に該当しているようだ。

##jsonハンドリングクラス

```py:JsonHandler.py
import json


class JsonHandler:
    def __init__(self, jsonPath):
        # jsonファイル読み込み
        self.file = open(jsonPath, 'r', encoding="utf-8_sig")
        self.jsonData = json.load(self.file)

    # 一次ネストのjsonデータ取得
    def getParam_OneNest(self, args):
        return self.jsonData[args]

    def __del__(self):
        # ファイル閉じる
        self.file.close()

```

###引っ掛かった点①：エンコーディングについて

ファイル取込の際のエンコードの方法を検索しましたが、
こちらの記事を参考にさせていただいた。
[WindowsでCP932(Shift-JIS)エンコード以外のファイルを開くのに苦労した話 - Qiita](https://qiita.com/Yuu94/items/9ffdfcb2c26d6b33792e)

どうやらBOMの処理のためにencoding="utf-8_sig"という方法を採用する必要があるようだ。

##outlookハンドリングクラス

```py:OutlookHandler.py

import glob
import os

import win32com.client


class OutlookHandler:
    def __init__(self):
        self.outlook = win32com.client.Dispatch("Outlook.Application")
        self.newMail = None

    def create(self):
        self.newMail = self.outlook.CreateItem(0)

    def setting(self, argMailformat, argSenderaddress,
                argReceiveraddressset, argCc_Addressset,
                argBcc_Addressset, argTitle,
                argAttachedfilefolderpath, argBody):

        # ///メールアイテムに情報設定
        self.newMail.BodyFormat = argMailformat
        try:
            self.newMail._oleobj_.Invoke(*(64209, 0, 8, 0, self.outlook.Session.Accounts(argSenderaddress)))
        except:
            pass
        self.newMail.To = argReceiveraddressset
        self.newMail.cc = argCc_Addressset
        self.newMail.Bcc = argBcc_Addressset
        self.newMail.Subject = argTitle
        # 本文に署名を付与（Outlook起動時のみ）
        try:
            self.newMail.Body = argBody \
                                + os.linesep \
                                + self.newMail.GetInspector.WordEditor.Bookmarks('_MailAutoSig').Range.Text
        except:
            self.newMail.Body = argBody

        # 添付ファイル取得
        if os.path.isdir(argAttachedfilefolderpath):
            for file in glob.glob(argAttachedfilefolderpath + '\\*'):
                if os.path.isfile(file):
                    self.newMail.Attachments.Add(file)

    def display(self):
        self.newMail.Display(False)

    def release(self):
        self.newMail = None
        self.outlook = None


```

###引っ掛かった点②：差出人メールアドレス設定について
こんなへんてこなやり方をしている。

```
self.newMail._oleobj_.Invoke(*(64209, 0, 8, 0, self.outlook.Session.Accounts(argSenderaddress)))
```

VBAの場合に定石だった
「SentOnBehalfOfName」フィールドがなぜか使えなかった（outlook側の設定の問題かもしれないけど）。
調べたらこんな記事がヒットしたため、それを採用。
[https://stackoverflow.com/questions/35908212/python-win32com-outlook-2013-sendusingaccount-return-exception](https://stackoverflow.com/questions/35908212/python-win32com-outlook-2013-sendusingaccount-return-exception)

ただしこの方法は、jsonで設定したアドレスがoutlookにユーザーのアドレスとして登録されていることが前提。
もし登録されていなかったら例外が発生するのでtrycatchで処理している。
SentOnBehalfOfNameのように登録していないものでも使えるというわけではない。
そのため、改良の余地はある。
（デフォルトのアドレスしか使わない場合は特に問題もない）

###引っ掛かった点③：本文に署名を乗せる方法について

```
self.newMail.Body = argBody \
                            + os.linesep \
                            + self.newMail.GetInspector.WordEditor.Bookmarks('_MailAutoSig').Range.Text  # 本文に署名を付与

```
このようにWordEditor.Bookmarksのフィールドを使用した方法だと、
「※outlookアプリケーション起動時にしか署名が付与されない」という大きな問題がある。
もし起動していない状態でmainメソッドを実行したら例外が発生する。

そのため、今回はtrycatchで処理して、
もし起動してないなら署名は諦めることにしている。

これも改良の余地がある部分だけど、仕方ない（outlook起動処理はそれはそれでけっこうめんどくさいので）。

##mainメソッド

```py:OutloookMailCreater.py
import datetime
import sys

# モジュール検索パスを再設定
# ダブルクリックによる起動にも対応できるようにする
sys.path.append(r"C:\Users\UserName\PycharmProjects\CreateToolAndTest")
import os
import tkinter
from tkinter import messagebox
from PrivateTool.OutlookMailCreater.JsonHandler import JsonHandler
from PrivateTool.OutlookMailCreater.OutlookHandler import OutlookHandler
import traceback

if __name__ == '__main__':

    try:

        # messageウィンドウの準備
        root = tkinter.Tk()
        root.withdraw()
        root.attributes("-topmost", True)

        # outlook起動
        outlookHandler = OutlookHandler()
        outlookHandler.create()

        # jsonファイル操作オブジェクト
        jsonHandler = JsonHandler(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mailSetting.json'))

        # jsonから値を取得しリストに格納
        settingList = [jsonHandler.getParam_OneNest('mailFormat'), ]
        settingList.append(jsonHandler.getParam_OneNest('senderAddress'))
        tempSet = jsonHandler.getParam_OneNest('receiverAddressSet')
        settingList.append('; '.join(tempSet))
        tempSet = jsonHandler.getParam_OneNest('CC_AddressSet')
        settingList.append('; '.join(tempSet))
        tempSet = jsonHandler.getParam_OneNest('BCC_AddressSet')
        settingList.append('; '.join(tempSet))
        settingList.append(jsonHandler.getParam_OneNest('title'))
        settingList.append(jsonHandler.getParam_OneNest('attachedFileFolderPath'))
        settingList.append(jsonHandler.getParam_OneNest('body'))

        # ◆◆◆リストデータになにか処理を加える場合はここに記述◆◆◆
        # myDate = datetime.date.today()
        # if myDate.day < 16:
        #     myDate -= datetime.timedelta(days=15)
        # settingList[5] = settingList[5].replace('$date', myDate.strftime('%Y%m'))
        # ◆◆◆◆◆◆

        # outlookの新規メールアイテム作成
        outlookHandler.setting(*settingList)

        # メールアイテムの表示
        outlookHandler.display()
        outlookHandler.release()

        # アナウンス
        messagebox.showinfo("", "正常終了")

    except:
        print("Error", messagebox.showinfo("Error", "エラー発生"))
        traceback.print_exc()
    finally:
        pass


```

###引っ掛かった点④：messageウィンドウを最前面に出す方法について
次のように設定してあげればいいらしい。

```
root.attributes("-topmost", True)
```


#5.終わりに

ツールとソースコードはこちら↓
[CreateToolAndTest/Tool_Python/OutlookMailCreater at master · dede-20191130/CreateToolAndTest](https://github.com/dede-20191130/CreateToolAndTest/tree/master/Tool_Python/OutlookMailCreater)

なにか補足がありましたらコメントください。