pythonでOutlookメールをフォルダに振り分ける

# はじめに
　日々メールっていっぱいきますよね、ハラ立つほどに。
　重要なメールに混じって読まずに速攻削除したいやつとか、一応見るけどやっぱ入らないやつとか、読んで処理したあととっておきたいやつとか。処理済みのメールをどうするかって話はいろいろ流派があると思うんですが、私はテーマごとのサブフォルダをつくってそこに放り込むことにしています。ちまちま1件ずつ移動するのもめんどくさいので自動でやってみようかと思い立ちました。
　Outlookにも振り分け用の機能はあるんですが、結構めんどくさいんですよね設定が。メンテナンス性も悪いし。
　ということで、pythonで作ってみました。振り分けの設定はjsonで記述しています。

## 前提
* Windows10
* Outlook2016
* Anaconda3
* Python3

## win32comのインストール

```
pip install pywin32
```

# outlookフォルダ構成
メール用のフォルダを作っておきます。

```:Outlook
xxx@aaa.com
  -受信トレイ
  -送信済みアイテム
  -削除済みアイテム
  -#01 カテゴリ1
    -01 サブテーマ1
  -#02 カテゴリ2
  -#99 archive
```

`#01`から下が振り分け用のフォルダです。フォルダ名先頭の`#01`は並び順用です。Outlookは数字より記号が先にくるので、`#`+`番号`とするときれいに並んでくれます。
　なお、Outlookフォルダ検索は部分一致で検索するので、なるべくフォルダ名がかぶらないのが推奨です。同じ名前だったり、複数ヒットする条件にした場合は先にヒットした方を対象にします。

# 振り分け設定ファイルの書き方
JSONで記述します。

```python:mail.json

{
  "cat1":{
    "subject":["カテゴリ1"],
    "address":[],
    "folder":"カテゴリ1",
    "unread":false
  },
  "del":{
    "subject":["怪しいタイトル","迷惑メール"],
    "address":["@ayashii.com"],
    "folder":"trush",
    "unread":true
  },
  ...
}
```

* "cat1"とか"del"は一意の識別子です。delだけは予約後になっており、削除（ゴミ箱）する対象です。ここの先頭に`$`をつけると処理対象外になります。
* "subject"はメールの件名です。カンマ区切りでOR条件検索します。正規表現で記載できます。
* ”address"は差出人または宛先のメールアドレスで、部分一致検索します。正規表現は使ってません。
* "folder"は振り分け先のフォルダ名です。上から順に小フォルダまで潜っていき、最初に部分一致したフォルダを振り分け先とします。
* "unread"は未読でも対象とするかどうかを設定します。迷惑メールなど問答無用で移動したければtrue、一応開封済みにするまでは振り分け対象都市な場合はfalseにします。
* 条件はOR条件です。AND条件には対応してません。経験上、これだけで結構いけます。あとは手動で。

# pythonコード
## 初期化
```python:mailmove.py
import win32com.client
import json
import re

# Outlook関係のオブジェクト初期化
app = win32com.client.Dispatch("Outlook.Application")
root = app.Session.DefaultStore.GetRootFolder()
ns = app.GetNamespace("MAPI")
inbox = ns.GetDefaultFolder(6)
messages = inbox.Items
```
* root:振り分け先フォルダの親フォルダ
* inbox:受信トレイ
* messages:受信トレイ内のメール一覧

## サブ関数群
```python:mailmove.py
# Outlookのフォルダ検索
def findfolder(root, name):
    for folder in root.Folders:
        # フォルダ名の部分一致
        if name in folder.name:
            print(folder.folderpath)
            return folder
        # サブフォルダも検索
        ret = findfolder(folder, name)
        if ret is not None:
            return ret
    return None

# 条件適合判定
def isit(message, subjects, addresses=[]):
    # 件名（正規表現）での判定
    for subject in subjects:
        if re.search(subject,message.subject)!=None:
            return True
    # メールアドレス（部分一致）判定
    for address in addresses:
        # 差出人名
        if address in message.sendername:
            return True
        # 差出人アドレス
        if address in message.senderemailaddress:
            return True
        # 宛先
        for recip in message.recipients:
            if address in recip.name or address in recip.address:
                return True
    return False

# アーカイブ先フォルダ検索
def whichFolder(message, dic):
    # 会議案内などは除外
    if message.messageClass == "IPM.Note":
        for key in dic:
            if isit(message, dic[key]["subject"], dic[key]["address"]):
                return key
    return None

# JSONファイルから移動条件をロードする
def load_json(filename="mail.json"):
    with open("mail.json", "r", encoding="utf-8") as f:
        dic = json.load(f)
        folders = {}
        for k in dic:
            # 識別名先頭が$ならコメント扱い
            if not k.startswith('$'):
                folders[k] = findfolder(root, dic[k]["folder"])
    return dic, folders
```

## メインの処理部
```python:mailmove.py
# メールのアーカイブ処理メイン部
def move_mail(
    dic, folders, target_folder=inbox, view_none=True, view_move=True, view_delete=True
):
    i = 1
    counter_move = 0
    counter_remain = 0
    counter_delete = 0
    list_move = list()
    for message in target_folder.Items:
        key = whichFolder(message, dic)
        # print(key)
        if key == "del":
            counter_delete += 1
            list_move.append((message, None))
        elif (
            key is None
            or folders[key] is None
            or folders[key].folderpath == target_folder.folderpath
        ):
            counter_remain += 1
            if view_none:
                print(counter_remain, "none", message.subject)
        elif dic[key]["unread"] or not message.unread:
            counter_move += 1
            if view_move:
                print(folders[key].name, message.subject)
            list_move.append((message, folders[key]))
        else:
            counter_remain += 1
            if view_none:
                print("unread", message.subject)
        i += 1
    for item in list_move:
        message = item[0]
        dest = item[1]
        if dest is None:
            if view_delete:
                print("delete", message.subject)
            message.delete()
        else:
            print(dest.name, message.subject)
            message.unread = False
            message.move(dest)
    print("moved:", counter_move, "delete:", counter_delete, "remain:", counter_remain)


# アーカイブ処理を全アーカイブ対象フォルダに対して実行
def do_all_folder(dic, folders):
    for k in dic:
        print(k)
        if k != "del":
            move_mail(dic, folders, target_folder=folders[k], view_none=False)
    print("do all done.")
```

## 処理実行部
処理実行は3種類ほど用意してあります。
### 受信トレイ(inbox)に対して実行
通常はこれを実行します。

```python:mailmove.py
dic, folders = load_json()

# 受信トレイ(inbox)に対して処理を行う場合
move_mail(dic,folders)
```

### 特定のフォルダに対して実行
特定フォルダのメールが増えてきた場合、さらに条件をつけて別フォルダにしたくなります。そんなとき、指定のフォルダに対して振り分け処理を再実行します。

```python:mailmove.py
dic, folders = load_json()

# 特定のフォルダに対して処理を行う場合
tf = findfolder(root, "xx")
move_mail(dic, folders, target_folder=tf)
```

### 全てのフォルダに対して実行
mail.jsonいじりすぎてわけがわからなくなったとき、全フォルダにたいして振り分け処理を実行してしまえ、って場合に使います。ただし、フォルダとメールが大量にあるとそれなりに時間かかるのでご注意を。

```python:mailmove.py
dic, folders = load_json()

# 全てのフォルダに対して再処理を行う場合
do_all_folder(dic,folders)
```

# batファイル実行

バッチファイル作っておくと便利です。定期的に実行しようかと思ったのですが、メール一通り開封したあと実行したいので今のところ手動で起動しています。
 プログラムはユーザプロファイルのフォルダ下のrepos/pyOfficeってフォルダに格納しています。
　python環境はanaconda3で構築しているので、activate.batを先に実行しています。

```bat:mailmove.bat
cd %USERPROFILE%\repos\pyOffice
call activate.bat
python mailmove.py
pause

```