morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】パス付きZIPファイルに圧縮してメールに添付&パスワードを別メールで送信の作業を自動化


パス付のZIPファイルを作成してoutlookのメールに添付、パスワードは別メールに記載して送信を自動で行う手法について解説します。

はじめに

うちの会社では社外にメールでファイルを送信したいときはパス付きのzipファイルを添付して、別のメールでパスワードを送るみたいな決まりがあります。
世間ではパス付きZIPファイルはあんまり安全性もないし、使うのやめましょうみたいな風潮ありますが、まだまだ使う会社も多いのではないでしょうか?
この作業が地味にめんどくさいので、自動化してみたいと思います。

会社ではWin10のパソコンでMicrosoft365のOutlookデスクトップアプリを使用していますのでそれで動くようにプログラム作ります。




パスワードを生成する

pythonで安全なパスワードを生成するにはsecretsモジュールを使用します。

ランダムに文字を並べればいいのでrandomモジュールでもいい気はしますが、公式ドキュメントによるとrandomモジュールより暗号学的に強い乱数を生成することができるそうです。

secretsモジュールとrandomモジュールの違い

secretsモジュールは結局のところrandom.SystemRandomオブジェクトを使ってるのでrandomモジュールの一種とも言えます。乱数生成でよく使うrandom.Randomオブジェクトとは乱数の生成方法が異なるようです。

random.Randomで生成できる乱数は疑似乱数といって、アルゴリズムにのっとってシード値から一意にきまる乱数です。最悪の場合シード値がわかればだれでも同じ乱数を作れるということです。

それに対してsecretsモジュールやrandom.SystemRandomの場合はOS が提供する最も安全な乱雑性のソースを使うのでそういった心配はないということみたいです。

パスワード生成のコード

それではsecretsモジュールを使ってパスワードを作成します。パスワードに使う文字は大文字小文字のアルファベット、数字とします。
パスワードに使う文字の指定はstringモジュールで簡単に作成できます

文字列定数 中身
string.ascii_letters 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
string.digits '0123456789'
string.punctuation '!"#$%&'()*+,-./:;<=>?@[\]^_`{ }~.'

これらの中からランダムに1文字ずつ取り出してパスワードを作成します。ランダムに取り出すにはsecrets.chpice()関数を使用します。

import string
import secrets

def create_pass(num):
    alphabet = string.ascii_letters + string.digits
    password = ''.join(secrets.choice(alphabet) for i in range(num))
    return password

print(create_pass(10))

出力は以下のようなパスワードが作成できました。
DMmXq6uKL7

パスワード付きZIPファイルを作成

パス付きのZIPファイルをpythonで作成するためにはpyminizipというライブラリがありますが、日本語のファイル名やフォルダ名を圧縮しようとするとうまくいきません。
なので7-ZIPというソフトをpythonで操作して圧縮していきます。

7-ZIPのインストール

以下のリンクからインストーラをダウンロードできます

https://www.7-zip.org/download.html

インストール後に生成される7z.exeをpythonから呼び出して使用します。

7-ZIPでパスワード付きZIPファイルを作成する

pythonで外部のアプリケーションを操作するには以前の記事でも書きましたが、subprocessモジュールを使用します。

mori-memo.hateblo.jp

パス付きのzipファイルを作成してみましょう。

フォルダ構成は以下にしておきました。

  • input: 圧縮したいファイルを入れとくフォルダ
    • テスト.txt
    • test2.txt
  • send: 圧縮後のファイルの出力先フォルダ
  • create_zip.py: 今回のコード

inputに入れたファイルを、自動生成したパスワードを用いてすべてtest.zipに圧縮して、sendフォルダ内に保存します。

import string
import secrets
import os
import glob
import subprocess

def create_pass(num):
    alphabet = string.ascii_letters + string.digits
    password = ''.join(secrets.choice(alphabet) for i in range(num))
    return password

pass_word = create_pass(10)
print(pass_word)

input_dir_path = os.path.join(os.path.dirname(__file__), "input") # 圧縮対象フォルダ
output_dir_path = os.path.join(os.path.dirname(__file__), "send") # 圧縮後のフォルダ

path_exe = r'C:\Program Files\7-Zip\7z.exe'# 7zip実行ファイルのパス
args = (
       path_exe, 
       'a', 
       os.path.join(output_dir_path, 'test.zip'), # 圧縮後のファイル
       input_dir_path + '\\*', # 圧縮対象フォルダ
       '-mx=9', # 圧縮レベル0-9 9が最大
       '-p=' + pass_word, # パスワード
)

result = subprocess.run(args)

結果は以下です。無事Zipファイルができました。


Zipファイルの中身は以下になっており、パスワードもちゃんと設定されてました。コマンドラインに出力されたパスワードでちゃんと解除も可能でした。





Outlookに添付ファイルを張り付ける

前回の記事にてOutlookで新規にメールを作成する方法について触れましたが、それを応用していきます。


メールのあて先はユーザがその都度変更できるようにしておきたいので、プログラムの中に書くのは少々手間です。なのでそこだけ手動にしておきましょう。

全体の流れは以下のようにしようと思います。

  1. Outlook側で手動操作
    1. Outlook側を操作して新規メール作成or返信作成(メール作成画面をポップアップさせて開いておく)
    2. Outlook側で宛先を入れる
  2. pythonコード実行
    1. 送信したいファイルを圧縮
    2. python側で開いておいたメールにファイル添付
    3. 開いておいたメールから宛先を取得
    4. 新規でメール作成し、取得したアドレス情報を入力
    5. パスワードを本文に記載
    6. メール画面を表示
    7. パス付きZipファイルを添付したメールと、パスワードのお知らせメールが開く

outlookで開いておいたメールに添付ファイルを添付する

開いておいたメールから宛先を取得してみます。

outlookで手動で新規メールを作成し、aaa@aaa.comを宛先、bbb@bbb.com, ccc@ccc.comをCC, ddd@ddd.comをBCCにしておきました。
その新規メールをポップアップさせて開いたままにしておき、そこから情報を取り出します。

まずは開かれたメールオブジェクトを取得します。

import win32com.client

outlook = win32com.client.Dispatch('Outlook.Application')
current_mail = outlook.ActiveInspector().CurrentItem # 現在アクティブなoutlookウィンドウのメールオブジェクトを取得

これで取得できました。後は過去記事同様にここに添付ファイル等指定するだけです。

import win32com.client

outlook = win32com.client.Dispatch('Outlook.Application')
current_mail = outlook.ActiveInspector().CurrentItem # 現在アクティブなoutlookウィンドウのメールオブジェクトを取得

current_mail.subject = 'ファイル送付'
current_mail.Attachments.Add(os.path.join(output_dir_path, 'test.zip')) # 先ほど作成したZIPファイルを張り付け
# current_mail.display(True) はもともと開かれてるので不要です

outlookで開いておいたメールから宛先情報(アドレス)を取得する

同じ宛先に別のメールでパスワードのお知らせをしないといけないので、先ほど開いたメールオブジェクトから宛先を取得します。
メールオブジェクトから宛先一覧を取得するのはcurrent_mail.Recipientsプロパティで取得できます。全宛先がリストとして帰ってきます。

Recipientオブジェクトはaddressというプロパティを持ってますが、Recipient.addressで帰ってくるのは表示名のみです(連絡先の登録名)
aaa@aaa.comのようなアドレスを取得するには少々めんどくさいです。いかにやり方が載ってます。

それでは開いたメールから宛先のアドレスを取得してみます。

recip_list = current_mail.Recipients

# それぞれのあて先のアドレスおよびtoかccかbccかのtypeを取得する

PR_SMTP_ADDRESS = "http://schemas.microsoft.com/mapi/proptag/0x39FE001E"
for recip in recip_list:
    recip_adress = recip.PropertyAccessor.GetProperty(PR_SMTP_ADDRESS) # アドレス
    recip_type = recip.Type # to: 1, cc: 2, bcc: 3
    print(recip_adress, recip_type)

結果は以下です。

ddd@ddd.com 3
bbb@bbb.com 2
ccc@ccc.com 2
aaa@aaa.com 1

あとは別で新規メールを作成してそこにアドレスを指定するだけです。

# 新規メールを作成
info_mail = outlook.CreateItem(0)

recip_list = current_mail.Recipients

# それぞれのあて先のアドレスおよびtoかccかbccかのtypeを取得する

PR_SMTP_ADDRESS = "http://schemas.microsoft.com/mapi/proptag/0x39FE001E"
for recip in recip_list:
    recip_adress = recip.PropertyAccessor.GetProperty(PR_SMTP_ADDRESS) # アドレス
    recip_type = recip.Type # to: 1, cc: 2, bcc: 3
    if recip_type == 1:
        if len(info_mail.to):
            info_mail.to += '; ' + recip_address
        else:
            info_mail.to += recip_address
    elif recip_type == 2:
        if len(info_mail.cc):
            info_mail.cc += '; ' + recip_address
        else:
            info_mail.cc += recip_address
    else:
        if len(info_mail.bcc):
            info_mail.bcc += '; ' + recip_address
        else:
            info_mail.bcc += recip_address

# 件名と本文を記入
info_mail.bodyFormat = 2
info_mail.subject = 'パスワードのお知らせ'
info_mail.body = '''先ほど送付したパスワードを連絡します。
パスワードは下記のとおりです。
{}
よろしくお願いいたします。'''.format(pass_word)
info_mail.display(True)

これで想定していた動作を実現できました。




全コード

最後に全コード載せます。

import win32com.client
import string
import secrets
import os
import glob
import subprocess

def create_pass(num):
    alphabet = string.ascii_letters + string.digits
    password = ''.join(secrets.choice(alphabet) for i in range(num))
    return password

pass_word = create_pass(10) # パスワード

# 送信したいファイルをパスワード付きZIPファイルに圧縮
input_dir_path = os.path.join(os.path.dirname(__file__), "input") # 圧縮対象フォルダ
output_dir_path = os.path.join(os.path.dirname(__file__), "send") # 圧縮後のフォルダ
output_dir_path = os.path.join(output_dir_path, 'file.zip') # 圧縮後のファイル

path_exe = r'C:\Program Files\7-Zip\7z.exe'# 7zip実行ファイルのパス
args = (
       path_exe, 
       'a', 
       output_dir_path, # 圧縮後のファイル
       input_dir_path + '\\*', # 圧縮対象フォルダ
       '-mx=9', # 圧縮レベル0-9 9が最大
       '-p=' + pass_word, # パスワード
)

result = subprocess.run(args)

outlook = win32com.client.Dispatch('Outlook.Application')

# 開いているメールにzipファイル貼り付け
current_mail = outlook.ActiveInspector().CurrentItem # 現在アクティブなoutlookウィンドウのメールオブジェクトを取得

current_mail.subject = 'ファイル送付'
current_mail.Attachments.Add(os.path.join(output_dir_path, 'file.zip')) # 先ほど作成したZIPファイルを張り付け

# パスワードお知らせ用新規メールを作成
info_mail = outlook.CreateItem(0)

# もともと開いていたメールから宛先のアドレス情報を取得し、新規メールにコピペ
recip_list = current_mail.Recipients
PR_SMTP_ADDRESS = "http://schemas.microsoft.com/mapi/proptag/0x39FE001E"
for recip in recip_list:
    recip_adress = recip.PropertyAccessor.GetProperty(PR_SMTP_ADDRESS) # アドレス
    recip_type = recip.Type # to: 1, cc: 2, bcc: 3
    if recip_type == 1:
        if len(info_mail.to):
            info_mail.to += '; ' + recip_address
        else:
            info_mail.to += recip_address
    elif recip_type == 2:
        if len(info_mail.cc):
            info_mail.cc += '; ' + recip_address
        else:
            info_mail.cc += recip_address
    else:
        if len(info_mail.bcc):
            info_mail.bcc += '; ' + recip_address
        else:
            info_mail.bcc += recip_address

# 件名と本文を記入
info_mail.bodyFormat = 2
info_mail.subject = 'パスワードのお知らせ'
info_mail.body = '''先ほど送付したパスワードを連絡します。
パスワードは下記のとおりです。
{}
よろしくお願いいたします。'''.format(pass_word)
info_mail.display(True)

終わりに

何とか完成しました。pythonでのドキュメントがなかったので少々苦戦しました。