こんにちは。ざわかける!のざわ(@zw_kakeru)です。
今回はpythonを用いてテキストファイルの中身を取得し、その内容を取り込んだOutlookメールを自動で送信(およびメール削除)するスクリプトを作成しました。
動的な文章をテキストファイルに分けることで、バッチファイルでの実行を可能にしてあります。
プログラミングができるとこんな風にラクができるんだなあ…と思ってください。
動作環境
python: 3.8.3
起こったこと
先日、私の所属している部署の偉い人から一通のメールが届きました。
(内容は一部変更を加えています。実際はもっとカッチリした文章でした。)
<busho-no-erai-hito@kaisha.com>からのメール: 皆さん こんにちは。元気? テレワークが続いてお互いにコミュニケーションが取れていないだろうから、 明日から、勤務の開始時と終了時に部署全員に報告のメールを送るようにしましょう! ルールは次の通りです。 1. 勤務開始・終了時に部署のメーリスに「勤務開始します」「勤務終了しました」のメールを出しましょう。 2. 勤務開始時はその日の予定(勤務時間・場所・作業内容)を、終了時にはその日の実績(勤務時間・場所・作業内容)をそれぞれ書いてください。 3. 勤務終了時には上記に加えて、何か一言(その日感じたこと・起こった出来事など)を添えてください。 以上、よろしくねんっ! 部署の偉い人より
…いやいや、昭和かっ!
本格的なテレワークが始まって一年以上が経過し、色々問題点が見えてきたのは分かるんですが如何せんその解決策に頭を抱えずにはいられませんね。
これのどこが世界最先端を目指すIT企業なんだよ、、、
私の所属している部署の人数は現在30人ほどなので、この翌日から毎日朝晩30通ずつの誰も読まないメールが送られあうという地獄絵図が展開されています。
こんなもの、愚直に毎日手作業で送っていたらとんでもないストレスになります。
さっさと自動化しなきゃ。
やりたいこと(仕様)
早速自動化のためのプログラムを書いていきたいのですが、その前にやりたいことを整理します。
メール本文に「コミュニケーションが取れていないだろうから」とあるように、この一連の作業は社員同士のコミュニケーション促進が目的なのです。
このメールで勤務実態を把握したり残業をつけたりといったことは無く、実際一日くらいサボっても特に文句は言われません。
であるとするならば、皆誰が何時にどこで何をしてるとかいうことには興味がないのです。
最も大事なのは3番のルールで、それ以外の部分は正直誰も読まないと考えてもいいでしょう。
(実際この運用が数日間行われていますが、私自身、他の社員からのメールはこの「終わりの一言」の部分しか読みません。逆にここは毎日皆好き放題書いていて結構面白かったりします。)
Pythonを使って自動化する
そもそも私はメールというシステム自体が昭和(平成)の古代遺物だと思っています。
いちいちタイトルと宛先を指定して、「お疲れ様です。〇〇です。」とか意味のない語句を並べて、文を書いてからミスがないか何度も確認を行う。一度送信したら後から取消し・編集もできない。
受け取る側もそれぞれのメールを一つ一つクリックして開かないと内容を確認できない。
気軽にスタンプを押すこともできないから「了解です。」ってことを伝えるだけでもわざわざ一通したためる必要がある。
もちろん他社とのやりとりなど、ある程度距離のある人とカッチリとしたやりとりを行うツールとしてはメールが適していると思いますが、こんな社内のどうでもいい(わりに毎日行う必要のある)連絡に、時代に取り残されつつあるレガシーなシステムなんか使うべきではないんですよ。
「コミュニケーションをとる」が目的なら今やそれにもっと適したツールがあるんです。普段友達に連絡をとるときにメールなんか使いますか、って話です。
閑話休題。
実装言語はPythonに決めました。理由は一番簡単に作れそうだったからです。私自身がPythonに馴れているっていうのもあります。
幸いにもPythonからOutlookをいじれるライブラリwin32comが出ているので、これを使っていこうと思います。
テキストファイルから「終わりの一言」を読み込む
このタスクを自動化するにあたって一番考えなければならないのは、動的コンテンツの生成方法です。
毎日同じメールを送るだけなら話はもっと単純なのですが、「終わりの一言」の部分は変えなければならないのです。
めんどくさい、、、けど先述しましたがここの一言に関しては私も価値を感じているので、うまく実装してあげたいですね。
一言自体は自分で考えるとして、それをどのようにしてメールに取り込むか、という部分が問題です。
gitのcomit messageよろしくコマンドラインから引数として直接渡すことも考えましたが、ターミナルをいちいち起動してディレクトリ移動したりするのもめんどくさかったのでここはテキストファイルから取り込むことにしました。
デスクトップに置いたテキストファイルに一言を書いて保存し、同じくデスクトップに置いたバッチファイルを実行する際にそのテキストファイルを開いてからメールを作る、という構成でいきましょう。
送信したメールを削除する
送信自体は上記の考え方で問題なさそうですが、毎日送信フォルダにこのメールが溜まっていくと整理するのも一苦労です、たまったもんじゃありません。
(溜まっていくとたまったもんじゃない、、)
なので、送信と同時にその今送ったメールを送信フォルダから削除されるようにしたいですね。
開始メールは送らないことにする
これは前述の通り、意味がないので送らないことにしました。
もちろん開始メールは一日ごとに内容が変わらない静的コンテンツなので終了メールより簡単に自動化することはできますが、誰も読まないメールを送っても誰も得しないのでこっそりサボろうと思います。
何か言われたらその時自動化してやりましょう(多分言われないけど)。
やったこと(実装)
ここからは実際のコーディング内容について記述していきます。
前日に送った終了メールを送信トレイから削除する
本日の終了メールを送る前に、昨日送った終了メールを送信トレイから削除しておきます。
上に書いたように本当はメールを送った直後にそのメールを削除するという実装をしたかったのですが、いざ実装しようとするとwin32comではメールを送ると自動的にBool値(true)が返される仕様になっており、そこで処理が終わってしまいメール削除の処理を実行することができませんでした。
(この辺はもう少し調べたら解決策があるのかもしれません。)
def deletePreviousMail(subject):
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
mails = outlook.GetDefaultFolder(5)
for i in range(10):
if mails.items[i].subject == subject:
mails.items[i].Delete()
break
自分の送信トレイから直近のメール10通を調べ、引数で指定されたタイトルと一致したメールを削除します。
もし10通の中にそのタイトルのメールが複数あった場合でも一番新しいメールしか削除されません。
アルゴリズム的に抜けがあるような気もしますが、消されずに残ってしまう分には取り立てて重要視する必要もないでしょう(消したくないメールまで勝手に消される、とかなら困りますが)。
ちなみに3行目にあるように、送信トレイのGetDefaultFolderのインデックスは5でした。
このインデックスの調査についてはTipsとして別記事にまとめておきました。
テキストファイルから「終わりの一言」を読み込む
「終わりの一言」を記述するテキストファイルmail_out_tweet.txtは次のような感じです。
失礼、噛みました。 うまぴょいwwうまぴょいww 今日はラーメンを食べに行きました。美味しかったです。 (。。。以下こんな感じで延々と続く。)
いたって普通のテキストファイルです。
このファイルからテキストを取り込むメソッドを実装しましょう。
def getTweet(file):
with open(file, 'r', encoding='utf-8') as f:
line = f.readline()
return line.strip()
with – openを使った基本的なファイルアクセスですね。
ファイルパスは引数で受け取ります。コマンドラインから指定してmainメソッドでパースし、そのままここに引き渡す形です。
メソッド名をTweet、としているのには特に深い意味はありません。何となくツイッターぽいなあと思ったからですね。
読み込みにはreadline()を用いました。これを一度だけ呼び出して値を返すことでこのテキストファイルの一行目だけを抽出することができます。
自分が過去にどんな一言を書いたのか、せっかくなので残しておこうかなと思ったのでこういう実装にしました。毎日このファイルの一行目にどんどん追加していき、古い一言は下へ下へと追いやられていくイメージですね(この辺もツイッターを意識していたりします)。
メールを作成し、送信する
最後に、メールを作成・送信するメソッドを実装します。
def sendMail(tweet):
outlook = win32com.client.Dispatch("Outlook.Application")
mail = outlook.CreateItem(0)
mail.to = 'busho-no-mailing-list@kaisha.com'
mail.subject = 'テレワーク終了'
mail.bodyFormat = 1
mail.body = '''皆様
ざわです。お疲れ様です。
業務終了します。
時間 : 8:30 - 17:30
場所 : 自宅
作業 : 打ち合わせ、資料作成
# {0}
以上
'''.format(tweet)
#mail.display(True)
mail.send()
こんな感じですかね。
作業時間と作業内容は厳密に言えば日によって変わるんですけど、まあこう書いとけば文句は出ないでしょう。
間に差し込む一言は、引数で受けたstringを.format()で読み込むことにします。
26行目でコメントアウトしてあるmail.display()は、送信前にメールをoutlookで開いてくれるメソッドで、ここで自分で最終確認をしたのちに手動で送信ボタンを押す、という運用も可能です。
私は確認なんていらないのでコメントアウトしていますが、デバッグでかなりお世話になったので一応残してあります。
完成したスクリプト
ここまでできたらあとは必要モジュールをインポートして、処理したい順番通りにmainメソッドを書いてあげるだけです。
最終的に完成したスクリプトmail_out.pyがこちらになります。
import win32com.client
import sys
def sendMail(tweet):
outlook = win32com.client.Dispatch("Outlook.Application")
mail = outlook.CreateItem(0)
mail.to = 'busho-no-mailing-list@kaisha.com'
mail.subject = 'テレワーク終了'
mail.bodyFormat = 1
mail.body = '''皆様
ざわです。お疲れ様です。
業務終了します。
時間 : 8:30 - 17:30
場所 : 自宅
作業 : 打ち合わせ、資料作成
# {0}
以上
'''.format(tweet)
#mail.display(True)
mail.send()
def deletePreviousMail(subject):
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
mails = outlook.GetDefaultFolder(5)
for i in range(10):
if mails.items[i].subject == subject:
mails.items[i].Delete()
break
def getTweet(file):
with open(file, 'r', encoding='utf-8') as f:
line = f.readline()
return line.strip()
if __name__ == '__main__':
deletePreviousMail('テレワーク終了')
file_path = sys.argv[1]
tweet = getTweet(file_path)
sendMail(tweet)
実行結果
これをバッチファイルにまとめることで、デスクトップからワンクリックでメールが送れるようになりました。
$python mail_out.py mail_out_tweet.txt
めでたしめでたし。(めでたくない)
終わりに
この毎日メールシステムを考案した部署の偉い人は、営業担当の方です。
(こんなことばっかりしてるから営業の人たちは毎日遅くまで残業しているんじゃ、、、)
開発担当を舐めんなよって感じですね。
これくらいのちょっとしたスクリプトは『技術』タグに入れるべきかどうかも迷ってしまいますが。
もうあと数週間もしたら徐々にサボり始める社員が出てくるだろうから、それに乗じてしれっとフェードアウトしよう。。
今後こんなアホなルールができた時にも、意地でも自動化して記事に書いてやろうと決意しました。
せめてチャットツールにしてほしいです。