0
0

「mapitool.exe」(ruby-msg)を代替するPython版mapitool(暫定)の作成

Last updated at Posted at 2024-08-15

1. きっかけ

こちらの『IIJ セキュアMX』Windows環境で動作する「SMX迷惑メール誤判定報告ツール」を作成しておきましたにて、
OutlookのMSG形式メールをeml形式に変換するツールとして、
rubyで作成された「mapitool-1.5.0-mswin32-stand_alone.zip」(ruby-msg)を使っていました。

(cles::blog 平常心是道 様).msg を .eml に変換する (ruby-msg編)

(Githubリポジトリ) https://github.com/aquasync/ruby-msg
(Google Code Archive)https://code.google.com/archive/p/ruby-msg

(Qiita)Outlook のメールデータ:インポート・エクスポート

しかし、件名(Subject)等ヘッダー部分や、本文に、日本語(所謂2バイト文字)が含まれていると、
文字化けしたり、その行ごと削除してしまう事象がありました。
そして、このプロジェクトは2012年から更新が無く、改修される目途はありませんでした。

そこで、ChatGPTを使って、Pythonによる置き換えを図ったところ、
とりあえず上記の問題は解決できるものは作成できたので記録しておきます。
※先のmapitool.exeもこのPythonコードをPyinstallerでexe化したものに置き換えました。

2. mapitool.py(暫定)ソースコード

#mapitool.py
#mapitool -i [filename.msg] しか動作確認できていません
#ruby-msg1.5.0版にあったヘッダーと本文の日本語文字化け問題に対応
#テキスト形式変換のみ対応

import os
import argparse
import extract_msg
import pysnptools
from datetime import datetime
from email.header import Header
from email.utils import formataddr

def custom_formataddr(name,addr):
   return formataddr((str(Header(name, 'utf-8')), addr))
 
class Mapitool:
    def __init__(self, files, opts):
        self.files = files
        self.opts = opts
        if not files:
            raise ValueError("Must specify 1 or more input files.")
        
        for f in files:
            ext = os.path.splitext(f.lower())[1][1:]
            if ext not in ['msg', 'pst']:
                raise ValueError(f"Unsupported file type - {f}")
            if ext == 'pst' and not opts.enable_pst:
                raise ValueError("Experimental PST support not enabled")
        
        if opts.output_dir:
            os.makedirs(opts.output_dir, exist_ok=True)

    def each_message(self):
        for filename in self.files:
            ext = os.path.splitext(filename.lower())[1][1:]
            if ext == 'pst':
                if self.opts.filter_path:
                    filter_path = self.opts.filter_path.replace("\\", '/').strip('/')
                else:
                    filter_path = None
                
                with open(filename, 'rb') as f:
                    pst = pysnptools.PSTReader(f)
                    for message in pst.messages():
                        if filter_path and not message.path.startswith(filter_path):
                            continue
                        yield message
            else:
                msg = extract_msg.Message(filename)
                yield msg

    def make_unique(self, filename):
        if not hasattr(self, 'map'):
            self.map = {}
        if not self.opts.individual and filename in self.map:
            return self.map[filename]
        
        try_name = filename
        i = 1
        while os.path.exists(try_name):
            try_name = f"{os.path.splitext(filename)[0]}.{i}{os.path.splitext(filename)[1]}"
            i += 1
        self.map[filename] = try_name
        return try_name

    def process_message(self, message):
       if isinstance(message, extract_msg.Message):
           mime_type = 'msg'
           filename= message.filename.replace(".msg", ".eml")
       else:
           mime_type = 'pst'
           filename =message.subject.replace("","_") + ".eml"

#        if self.opts.individual:
#            filename = filename.replace("", "_")
#        else:
#            filename = "Mail.mbox"

       dir=self.opts.output_dir if self.opts.output_dir else ""
       filename= os.path.join(dir, filename)
       filename= self.make_unique(filename)

       with open(filename, 'w', encoding='utf-8')as f:
           if mime_type == 'msg':
               sender_name,sender_addr= message.sender.split('<')if'<' in message.sender else('', message.sender)
               sender_addr = sender_addr.strip('>')
               f.write(f"From: {custom_formataddr(sender_name.strip(), sender_addr)}\n")

               to_addresses= message.to.split(';')if message.to else []
               formatted_to = [custom_formataddr(addr.split('<')[0].strip(), addr.split('<')[1].strip('>')) for addr in to_addresses if'<' in addr]
               f.write(f"To: {', '.join(formatted_to)}\n")

               cc_addresses = message.cc.split(';') if message.cc else []
               formatted_cc = [custom_formataddr(addr.split('<')[0].strip(), addr.split('<')[1].strip('>'))for addr in cc_addresses if'<' in addr]

               if formatted_cc:
                   f.write(f"Cc: {', '.join(formatted_cc)}\n")


               f.write(f"Subject: {str(Header(message.subject,'utf-8'))}\n")
               f.write(f"Date: {message.date}\n")
               f.write("Content-Type: text/plain; charset=utf-8\n")
               f.write("\n")  # ヘッダーと本文の間の空行
               body_lines = message.body.splitlines()
               cleaned_body = '\n'.join(line.rstrip() for line in body_lines)
               f.write(cleaned_body)
           else:
               #PSTファイルの場合の処理(必要に応じて調整)
               f.write(message.body.rstrip())  # 末尾の余分な改行を削除

       if self.opts.verbose:
           print(f"Converted:{filename}")

    def run(self):
        for message in self.each_message():
            self.process_message(message)

def main():
    parser = argparse.ArgumentParser(description="Mapitool: Convert msg and pst files to standard formats")
    parser.add_argument('files', metavar='FILE', nargs='+', help='Input files')
    parser.add_argument('-o', '--output-dir', help='Put all output files in DIR')
    parser.add_argument('-i', '--individual', action='store_true', help='Do not combine converted files')
    parser.add_argument('-s', '--stdout', action='store_true', help='Write all data to stdout')
    parser.add_argument('-f', '--filter-path', help='Only process pst items in PATH')
    parser.add_argument('--enable-pst', action='store_true', help='Turn on experimental PST support')
    parser.add_argument('-v', '--verbose', action='store_true', help='Run verbosely')
    # parser.add_argument('-h', '--help', action="help", help="Show this message")

    args = parser.parse_args()
    tool = Mapitool(args.files, args)
    tool.run()

if __name__ == '__main__':
    main()
    

以上です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0