メール送信機能を今更ながらPythonで実装してみた

## はじめに

今更ながらPythonでメールを送信する機能を[実装](#実装)しました！
ファイルの添付も簡単にできます！

## 使い方

gmailからのメール送信を例に使い方を説明します。

gmailの`user`, `password`を下記のリンクを参考に取得します。
[PythonでGmailを送信する](https://qiita.com/eito_2/items/ef77e44955e43f31ba78)

取得後に下記のプログラムを実行します。

```python

from_address = '送信元アドレス'
to_address = '送信先アドレス'
subject = '件名'
body = '本文'
# 添付するファイルのパス
attachment = 'test.txt'
user = 'user'
password = 'password'

mail = Mail('smtp.gmail.com', 587, user, password)
# 添付ファイルなし
mail.send(from_address, to_address, subject, body)
# 添付ファイルあり
mail.send(from_address, to_address, subject, body, attachments=attachment)
```

送信先のメールアドレス(cc, bccを含む)をリストで渡すと、複数人送信が可能です。
同じく、添付ファイルをリストで渡すと、複数のファイルを添付可能です。

## 終わりに

今更ながらですみません。。。

## 実装

```python
import copy
import os
import smtplib
from email.header import Header
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate
from typing import Union


class Mail(object):
    """
    メール送信を行うクラス
    """

    def __init__(self, smtp_host: str, smtp_port: int, user: str = None, password: str = None):
        """コンストラクタ

        Args:
            smtp_host: smtpのホスト
            smtp_port: smtpのポート
            user: ユーザ
            password: パスワード
        """
        self.smtp_host = smtp_host
        self.smtp_port = smtp_port
        self.user = user
        self.password = password

    def send(self, from_address: str, to_addresses: Union[str, list], subject: str, body: str,
             attachments: Union[str, list] = None, profile: str = None, cc_addresses: Union[str, list] = None,
             bcc_addresses: Union[str, list] = None):
        """メールを送信する

        Args:
            from_address: 送信元アドレス
            to_addresses: 送信先アドレス
            subject: 件名
            body: 本文
            attachments: 添付ファイル
            profile: プロファイル
            cc_addresses: ccアドレス
            bcc_addresses:　bccアドレス

        """

        if attachments:
            msg = self.create_body_with_attachment(
                from_address, to_addresses, subject, body, attachments, profile, cc_addresses, bcc_addresses)
        else:
            msg = self.create_body(from_address, to_addresses, subject, body, profile, cc_addresses, bcc_addresses)

        if type(to_addresses) is list:
            send_list = copy.copy(to_addresses)
        else:
            send_list = [to_addresses]

        if cc_addresses:
            if type(cc_addresses) is list:
                send_list = send_list + cc_addresses
            else:
                send_list = send_list + [cc_addresses]

        if bcc_addresses:
            if type(cc_addresses) is list:
                send_list = send_list + bcc_addresses
            else:
                send_list = send_list + [bcc_addresses]

        smtpobj = smtplib.SMTP(self.smtp_host, self.smtp_port)
        smtpobj.ehlo()
        smtpobj.starttls()
        smtpobj.ehlo()
        if self.password:
            smtpobj.login(self.user, self.password)
        smtpobj.sendmail(from_address, send_list, msg.as_string())
        smtpobj.close()

    def create_body_with_attachment(self, from_address, to_addresses, subject, body, attachments, profile,
                                    cc_addresses=None, bcc_addresses=None):
        """添付ファイル付きのメール本文を作成する

        Args:
            from_address: 送信元アドレス
            to_addresses: 送信先アドレス
            subject: 件名
            body: 本文
            attachments: 添付ファイル
            profile: プロファイル
            cc_addresses: ccアドレス
            bcc_addresses:　bccアドレス

        Returns:
            メール本文
        """

        msg = MIMEMultipart()
        msg = self.create_msg(msg, from_address, to_addresses, subject, profile, cc_addresses, bcc_addresses)
        body = MIMEText(body.encode("utf-8"), 'html', 'utf-8')
        msg.attach(body)

        if type(attachments) is not list:
            attachments = [attachments]

        for attachment in attachments:
            base_name = os.path.basename(attachment)
            with open(attachment, "rb") as f:
                part = MIMEApplication(
                    f.read(),
                    Name=base_name
                )

            part['Content-Disposition'] = 'attachment; filename="{}"'.format(base_name)
            msg.attach(part)

        return msg

    def create_body(self, from_address, to_addresses, subject, body, profile, cc_addresses=None, bcc_addresses=None):
        """メール本文を作成する

        Args:
            from_address: 送信元アドレス
            to_addresses: 送信先アドレス
            subject: 件名
            body: 本文
            profile: プロファイル
            cc_addresses: ccアドレス
            bcc_addresses:　bccアドレス

        Returns:
            メール本文
        """

        msg = MIMEText(body.encode("utf-8"), 'html', 'utf-8')
        return self.create_msg(msg, from_address, to_addresses, subject, profile, cc_addresses, bcc_addresses)

    @staticmethod
    def create_msg(msg, from_address, to_addresses, subject, profile=None, cc_addresses=None, bcc_addresses=None):
        msg['Subject'] = subject
        if profile:
            msg['From'] = '{} <{}>'.format(Header(profile.encode('utf-8'), 'utf-8').encode(), from_address)
        else:
            msg['From'] = from_address

        def to_string_addresses(addresses):
            if type(addresses) is list:
                return ','.join(addresses)
            else:
                return addresses

        msg['To'] = to_string_addresses(to_addresses)
        msg['Bcc'] = to_string_addresses(bcc_addresses)
        msg['Cc'] = to_string_addresses(cc_addresses)

        msg['Date'] = formatdate()

        return msg

```