瀏覽代碼

feat: 第一代版本

实现SMTP发件功能
实现IMAP收件功能
SongZihuan 2 年之前
當前提交
a442ed5038
共有 10 個文件被更改,包括 1159 次插入0 次删除
  1. 142 0
      .gitignore
  2. 二進制
      HuanMail.ico
  3. 322 0
      mailbox-cli.py
  4. 0 0
      mailbox/__init__.py
  5. 104 0
      mailbox/email.py
  6. 105 0
      mailbox/imap.py
  7. 380 0
      sender-cli.py
  8. 0 0
      sender/__init__.py
  9. 77 0
      sender/email.py
  10. 29 0
      sender/smtp.py

+ 142 - 0
.gitignore

@@ -0,0 +1,142 @@
+.idea
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+proarchive_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+*.json

二進制
HuanMail.ico


+ 322 - 0
mailbox-cli.py

@@ -0,0 +1,322 @@
+import json
+import imaplib
+from cmd import Cmd
+from typing import Optional
+import os
+
+from mailbox.email import Mail
+from mailbox.imap import Imap
+
+
+class CLManager(Cmd):
+    intro = 'Welcome to the HuanMail (IMAP).'
+    prompt = 'HuanMail>'
+    file = None
+
+    def __init__(self):
+        super(CLManager, self).__init__()
+        self.imap: Optional[Imap] = None
+
+    def do_load(self, path):
+        """Load setting from file."""
+
+        try:
+            with open(path, "r", encoding="utf-8") as f:
+                try:
+                    conf = json.load(f)
+                    imap = conf.get("imap")
+                    if imap:
+                        self.imap = Imap(
+                            user=imap["user"],
+                            passwd=imap["passwd"],
+                            host=imap.get("host", "localhost"),
+                            port=imap.get("port", 465),
+                            ssl=imap.get("ssl", True),
+                            start_ssl=imap.get("start_ssl", False)
+                        )
+
+                        self.imap.connect()
+                        self.imap.disconnect()
+
+                        self.imap.inbox = imap.get("inbox", "INBOX")
+                        mailbox = imap.get("mailbox", {})
+                        for num in imap.get("mailbox", {}):
+                            byte: str = mailbox[num]
+                            self.imap.add_mail(num, byte.encode("utf-8"))
+                except KeyError:
+                    print("Key error.")
+                except imaplib.IMAP4.error:
+                    print("Sorry, IMAP Authentication error. Please check your user and password.")
+                except Exception:
+                    print("Sorry, Unknown error.")
+                else:
+                    print("Okay.")
+        except FileNotFoundError:
+            print("File not fount.")
+        except IOError:
+            print("IO error.")
+
+    def do_save(self, path):
+        """Save setting to file."""
+
+        conf = {}
+        if self.imap:
+            conf["imap"] = {}
+            conf["imap"]["user"] = self.imap.user
+            conf["imap"]["passwd"] = self.imap.passwd
+            conf["imap"]["host"] = self.imap.host
+            conf["imap"]["port"] = self.imap.port
+            conf["imap"]["ssl"] = self.imap.ssl
+            conf["imap"]["start_ssl"] = self.imap.start_ssl
+            conf["imap"]["inbox"] = self.imap.inbox
+            
+            mailbox = {}
+            for i in self.imap.mailbox:
+                byte: bytes = i.byte
+                mailbox[i.num] = byte.decode("utf-8")
+
+            conf["imap"]["mailbox"] = mailbox
+
+        try:
+            with open(path, "w", encoding="utf-8") as f:
+                f.write(json.dumps(conf))
+        except FileNotFoundError:
+            print("File not found.")
+        except IOError:
+            print("IO error.")
+        else:
+            print("Okay.")
+
+    def do_login(self, arg):
+        """Login imap server."""
+        if len(arg) != 0:
+            print("Bad syntax.")
+            return
+
+        user = input("User:")
+        passwd = input("Passwd:")
+        host = input("Host[localhost]:")
+        port = input("Port[993]:")
+        ssl = input("SSL[y]:")
+
+        if len(host) == 0:
+            host = "localhost"
+
+        if len(port) == 0:
+            port = 993
+        else:
+            try:
+                port = int(port)
+            except (ValueError, TypeError):
+                print("Port must be number")
+                return
+
+        if len(ssl) == 0 or ssl == "y":
+            ssl = True
+        else:
+            ssl = False
+
+        print(f"""Login imap
+Host: {host}:{port}
+User: {user}
+Passwd: {passwd}
+SSL: {ssl}
+Sure? [Yes/No]""")
+
+        if input() == "No":
+            print("Stop.")
+            return
+
+        try:
+            self.imap = Imap(user, passwd, host=host, port=port, ssl=ssl)
+            self.imap.connect()
+            self.imap.disconnect()
+        except imaplib.IMAP4.error:
+            print("Sorry, IMAP Authentication error. Please check your user and password.")
+        except Exception:
+            print("Sorry, Unknown error.")
+        else:
+            print("Okay.")
+
+    def do_logout(self, arg):
+        if len(arg) != 0:
+            print("Bad syntax.")
+            return
+
+        if input("Sure?[Yes/No]") == "No":
+            print("Stop.")
+            return
+
+        self.imap = None
+        print("Okay.")
+
+    def do_info(self, arg):
+        """Show imap info."""
+
+        if len(arg) != 0:
+            print("Bad syntax.")
+            return
+
+        if self.imap:
+            print(f"Host: {self.imap.host}:{self.imap.port}")
+            print(f"User: {self.imap.user}")
+            print(f"Passwd: {self.imap.passwd}")
+            print(f"SSL: {self.imap.ssl}")
+            print(f"MailBox: {self.imap.inbox}")
+        else:
+            print("Not login.")
+
+    def do_get(self, arg):
+        """Get all mail from mailbox."""
+
+        if arg != "ALL":
+            print("Bad syntax.")
+            return
+
+        if not self.imap:
+            print("Please login first.")
+            return
+
+        print("Please wait...")
+        try:
+            self.imap.fetch_all()
+        except imaplib.IMAP4.error:
+            print("IMAP4 error, please check setting.")
+        else:
+            print("Okay.")
+
+    def do_show(self, arg):
+        """Show Mailbox"""
+
+        try:
+            start, step = arg.split()
+            start = int(start)
+            step = int(step)
+        except (TypeError, ValueError):
+            print("Bad syntax.")
+            return
+
+        if not self.imap:
+            print("Please login first.")
+            return
+
+        try:
+            mailbox = self.imap.mailbox[start:]
+            count = 0
+            for i in mailbox:
+                print(f"* {i}")
+                count += 1
+                if count == step:
+                    break
+        except IndexError:
+            print("Bad index.")
+        else:
+            print("Okay.")
+
+    def do_check(self, arg):
+        """check        ---    check mail text.
+check save   ---    check mail text and save.
+check source ---    check mail source and save.
+check file   ---    check mail file and save."""
+
+        if not self.imap:
+            print("Please login first.")
+            return
+
+        num = input("Mail number:")
+        mail = self.imap.fetch(num)
+
+        if len(arg) == 0:
+            self.check(mail)
+        elif arg == "save":
+            self.check_save(mail)
+        elif arg == "source":
+            self.check_source(mail)
+        elif arg == "file":
+            self.check_file(mail)
+        else:
+            print(f"Bad syntax.")
+
+    @staticmethod
+    def __print_check(mail: Mail):
+        print(f"Title: {mail.title}")
+        print(f"From: {mail.from_addr}")
+        print(f"Date: {mail.date}")
+
+    @staticmethod
+    def check(mail: Mail):
+        CLManager.__print_check(mail)
+        print(f"\n{mail.body}\n")
+
+    @staticmethod
+    def check_save(mail: Mail):
+        CLManager.__print_check(mail)
+        path = input("Path:")
+        try:
+            with open(path, "w", encoding="utf-8") as f:
+                f.write(mail.body)
+        except IOError:
+            print("IO error.")
+        else:
+            print("Okay.")
+
+    @staticmethod
+    def check_source(mail: Mail):
+        CLManager.__print_check(mail)
+        path = input("Path:")
+        try:
+            with open(path, "wb") as f:
+                f.write(mail.byte)
+        except IOError:
+            print("IO error.")
+        else:
+            print("Okay.")
+
+    def check_file(self, mail: Mail):
+        path = input("Path:")
+        os.makedirs(path, exist_ok=True)
+
+        try:
+            mail.save_file(path)
+        except IOError:
+            print("IO error.")
+        else:
+            print("Okay.")
+
+    def do_mailbox(self, arg):
+        if not self.imap:
+            print("Please login first.")
+            return
+
+        if arg == "show":
+            for i in self.imap.list():
+                print(f"* {i}")
+            print("Okay")
+        elif arg == "setting":
+            mailbox = input("Mailbox:")
+            try:
+                self.imap.inbox = mailbox
+            except imaplib.IMAP4.error:
+                print("Bad mailbox.")
+            else:
+                print("Okay")
+        else:
+            print("Bad syntax.")
+
+    def do_quit(self, _):
+        """Exit HuanMail."""
+
+        print("Bye~")
+        if self.file:
+            self.file.close()
+            self.file = None
+        return True
+
+
+if __name__ == '__main__':
+    manager = CLManager()
+    try:
+        manager.cmdloop()
+    except KeyboardInterrupt:
+        print("\nBye~")
+        quit(0)

+ 0 - 0
mailbox/__init__.py


+ 104 - 0
mailbox/email.py

@@ -0,0 +1,104 @@
+from email import message_from_bytes
+import email.header
+import os
+import re
+import datetime
+import calendar
+
+
+class Mail:
+    date_pattern = re.compile(
+        r"[A-Za-z]+, "
+        r"([0-9]{1,2}) "
+        r"([A-Za-z]+) "
+        r"([0-9]{4}) "
+        r"([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}) "
+        r"([\s\S]*)"
+    )
+
+    time_zone_pattern = re.compile(r"([+-])([0-9]{2})00")
+
+    def __init__(self, num: str, data: bytes):
+        self.__data = message_from_bytes(data)
+        self.byte = data
+        self.num = num
+
+    @property
+    def from_addr(self):
+        if not self.__data['From']:
+            return ""
+        return str(email.header.make_header(email.header.decode_header(self.__data['From'])))
+
+    @property
+    def date(self):
+        if not self.__data['Date']:
+            return datetime.datetime(2022, 1, 1)
+        date = str(email.header.make_header(email.header.decode_header(self.__data['Date'])))
+        res = self.date_pattern.match(str(date)).groups()
+        time = datetime.datetime(int(res[2]),
+                                 list(calendar.month_abbr).index(res[1]),
+                                 int(res[0]),
+                                 int(res[3]),
+                                 int(res[4]),
+                                 int(res[5]))
+
+        timezone = self.time_zone_pattern.match(res[6])
+        if timezone:
+            if timezone.groups()[0] == '-':
+                time += datetime.timedelta(hours=int(timezone.groups()[1]))
+            else:
+                time -= datetime.timedelta(hours=int(timezone.groups()[1]))
+            time += datetime.timedelta(hours=8)  # 转换为北京时间
+        return time
+
+    @property
+    def title(self):
+        if not self.__data['Subject']:
+            return ""
+        return (str(email.header.make_header(email.header.decode_header(self.__data['Subject'])))
+                .replace('\n', '')
+                .replace('\r', ''))
+
+    @property
+    def body(self):
+        return self.__get_body(self.__data).decode("utf-8")
+
+    def __get_body(self, msg):
+        if msg.is_multipart():
+            return self.__get_body(msg.get_payload(0))
+        else:
+            return msg.get_payload(None, decode=True)
+
+    def save_file(self, file_dir: str):
+        return self.__get_files(self.__data, file_dir)
+
+    @staticmethod
+    def __get_files(msg, file_dir: str):
+        create = False
+        for part in msg.walk():
+            if not create:
+                os.makedirs(file_dir, exist_ok=True)
+                create = True
+
+            if part.get_content_maintype() == 'multipart':
+                continue
+            if part.get('Content-Disposition') is None:
+                continue
+            filename = part.get_filename()
+
+            if filename:
+                filepath = os.path.join(file_dir, filename)
+                with open(filepath, 'wb') as f:
+                    f.write(part.get_payload(decode=True))
+
+    def __lt__(self, other: "Mail"):
+        return self.date < other.date
+
+    def __eq__(self, other: "Mail"):
+        return self.date == other.date
+
+    def __le__(self, other: "Mail"):
+        return self.date <= other.date
+
+    def __str__(self):
+        return f"{self.num} {self.title} {self.from_addr} {self.date}"

+ 105 - 0
mailbox/imap.py

@@ -0,0 +1,105 @@
+import imaplib
+import re
+from typing import List
+
+from .email import Mail
+
+
+imaplib.Commands['ID'] = 'AUTH'
+
+
+class Imap:
+    def __init__(self, user: str, passwd: str, host="localhost", port=993, ssl=True, start_ssl=False):
+        self.host = host
+        self.port = port
+        self.user = user
+        self.passwd = passwd
+        self.ssl = ssl
+        self.start_ssl = False if ssl else start_ssl
+        self.server: None | imaplib.IMAP4 = None
+        self.__mailbox = {}
+        self.__inbox = "INBOX"
+
+    @property
+    def inbox(self):
+        return self.__inbox
+
+    @inbox.setter
+    def inbox(self, inbox):
+        self.__inbox = inbox
+        self.connect()  # 测试连接
+        self.disconnect()
+        self.__mailbox = {}
+
+    def connect(self):
+        if not self.server:
+            if self.ssl:
+                self.server = imaplib.IMAP4_SSL(self.host, port=self.port)
+            else:
+                self.server = imaplib.IMAP4(self.host, port=self.port)
+                if self.start_ssl:
+                    self.server.starttls()
+            self.server.login(self.user, self.passwd)
+            args = ("name", "HuanMail", "contact", "songzihuan@song-zh.com", "version", "1.0.0", "vendor", "HuanMail")
+            self.server._simple_command('ID', '("' + '" "'.join(args) + '")')
+            self.server.select(self.__inbox.encode("utf-7").replace(b"+", b"&").decode("utf-8"))
+
+    def disconnect(self):
+        if self.server:
+            self.server.logout()
+            self.server = None
+
+    def search(self):
+        self.connect()
+        res = self.__search()
+        self.disconnect()
+        return res
+
+    def __search(self):
+        _, data = self.server.search(None, 'ALL')
+        return data[0].decode("utf-8").split()
+
+    def list(self):
+        self.connect()
+        res = self.__list()
+        self.disconnect()
+        return res
+
+    def __list(self):
+        res = []
+        mailbox_pattern = re.compile(r"\(.*\) \".*\" \"(.+)\"")
+        for i in self.server.list()[1]:
+            i: bytes
+            mailbox = re.match(mailbox_pattern, i.replace(b"&", b"+").decode("utf-7"))
+            if mailbox:
+                res.append(mailbox.groups()[0])
+        return res
+
+    def fetch(self, num: str) -> Mail:
+        self.connect()
+        res = self.__fetch(num)
+        self.disconnect()
+        return res
+
+    def __fetch(self, num: str):
+        mail = self.__mailbox.get(num)
+        if mail:
+            return mail
+
+        _, data = self.server.fetch(num, '(RFC822)')
+        mail = Mail(num, data[0][1])
+        self.__mailbox[num] = mail
+        return mail
+
+    def fetch_all(self):
+        self.connect()
+        for i in self.__search():
+            self.__fetch(i)
+        self.disconnect()
+
+    @property
+    def mailbox(self) -> List[Mail]:
+        return sorted(self.__mailbox.values(), reverse=True)
+
+    def add_mail(self, num: str, data: bytes):
+        self.__mailbox[num] = Mail(num, data)

+ 380 - 0
sender-cli.py

@@ -0,0 +1,380 @@
+import smtplib
+import json
+from cmd import Cmd
+from typing import Optional
+
+from sender.email import Email
+from sender.smtp import Sender
+
+
+class CLManager(Cmd):
+    intro = 'Welcome to the HuanMail (SMTP).'
+    prompt = 'HuanMail>'
+    file = None
+
+    def __init__(self):
+        super(CLManager, self).__init__()
+        self.sender: Optional[Sender] = None
+        self.email: Optional[Email] = None
+
+    def do_load(self, path):
+        """Load setting from file."""
+
+        try:
+            with open(path, "r", encoding="utf-8") as f:
+                try:
+                    conf = json.load(f)
+                    sender = conf.get("sender")
+                    if sender:
+                        self.sender = Sender(
+                            user=sender["user"],
+                            passwd=sender["passwd"],
+                            host=sender.get("host", "localhost"),
+                            port=sender.get("port", 465),
+                            debug=sender.get("debug", False),
+                            ssl=sender.get("ssl", True),
+                            start_ssl=sender.get("start_ssl", False)
+                        )
+
+                    email = conf.get("email", {})
+                    if email:
+                        self.email = Email(
+                            from_addr=(email["from_name"], email["from_addr"]),
+                            subject=input("Title:")
+                        )
+                except smtplib.SMTPAuthenticationError:
+                    print("Sorry, SMTP Authentication error. Please check your user and password.")
+                except smtplib.SMTPException:
+                    print("Sorry, SMTP error.")
+                except KeyError:
+                    print("Key error.")
+                except Exception:
+                    print("Sorry, Unknown error.")
+                else:
+                    print("Okay.")
+        except FileNotFoundError:
+            print("File not fount.")
+        except IOError:
+            print("IO error.")
+
+    def do_save(self, arg):
+        """Save setting to file."""
+
+        conf = {}
+        if self.sender:
+            conf["sender"] = {}
+            conf["sender"]["user"] = self.sender.user
+            conf["sender"]["passwd"] = self.sender.passwd
+            conf["sender"]["host"] = self.sender.host
+            conf["sender"]["port"] = self.sender.port
+            conf["sender"]["debug"] = self.sender.debug
+            conf["sender"]["ssl"] = self.sender.ssl
+            conf["sender"]["start_ssl"] = self.sender.start_ssl
+
+        if self.email:
+            conf["email"] = {}
+            conf["email"]["from_name"], conf["email"]["from_addr"] = self.email.from_addr
+
+        try:
+            with open(arg, "w", encoding="utf-8") as f:
+                f.write(json.dumps(conf))
+        except FileNotFoundError:
+            print("File not found.")
+        except IOError:
+            print("IO error.")
+        else:
+            print("Okay.")
+
+    def do_sender(self, arg):
+        """sender setting    --    setting the sender.
+sender show       --    show sender info.
+sender delete     --    delete the sender."""
+
+        if arg == "setting":
+            self.sender_setting()
+        elif arg == "show":
+            self.sender_show()
+        elif arg == "delete":
+            self.sender_delete()
+        else:
+            print(f"Bad syntax.")
+            return
+
+    def sender_show(self):
+        print(f"""Host: {self.sender.host}:{self.sender.port}
+User: {self.sender.user}
+Passwd: {self.sender.passwd}""")
+
+    def sender_setting(self):
+        user = input("User:")
+        passwd = input("Passwd:")
+        host = input("Host[localhost]:")
+        port = input("Port[465]:")
+        debug = input("Debug[n]:")
+        ssl = input("SSL[y]:")
+
+        if len(host) == 0:
+            host = "localhost"
+
+        if len(port) == 0:
+            port = 465
+        else:
+            try:
+                port = int(port)
+            except (ValueError, TypeError):
+                print("Port must be number")
+                return
+
+        if len(debug) == 0 or debug == "n":
+            debug = False
+        else:
+            debug = True
+
+        if len(ssl) == 0 or ssl == "y":
+            ssl = True
+        else:
+            ssl = False
+
+        print(f"""Setting sender
+Host: {host}:{port}
+User: {user}
+Passwd: {passwd}
+Debug: {debug}
+SSL: {ssl}
+Sure? [Yes/No]""")
+
+        if input() == "No":
+            print("Stop.")
+            return
+
+        try:
+            self.sender = Sender(user, passwd, host=host, port=port, debug=debug, ssl=ssl)
+        except smtplib.SMTPAuthenticationError:
+            print("Sorry, SMTP Authentication error. Please check your user and password.")
+        except smtplib.SMTPException:
+            print("Sorry, SMTP error.")
+        except Exception:
+            print("Sorry, Unknown error.")
+        else:
+            print("Okay.")
+
+    def sender_delete(self):
+        if input("Sure?[Yes/No]") == "No":
+            print("Stop.")
+            return
+
+        self.sender = None
+        print("Sender has been delete.")
+
+    def do_email(self, arg):
+        """email new         --    create a new email.
+email delete      --    delete the email.
+email text        --    add test to email.
+email text-file   --    add text from file to email.
+email html        --    add html to email.
+email html-file   --    add html from file to email.
+email file        --    add a file to email.
+email show        --    show the email info."""
+
+        if arg == "new":
+            self.email_new()
+        else:
+            if not self.email:
+                print("Please create email first.")
+                return
+
+            if arg == "delete":
+                self.email_delete()
+            elif arg == "text":
+                self.email_text()
+            elif arg == "text-file":
+                self.email_text_file()
+            elif arg == "html":
+                self.email_html()
+            elif arg == "html-file":
+                self.email_html_file()
+            elif arg == "file":
+                self.email_file()
+            elif arg == "show":
+                self.email_show()
+            else:
+                print(f"Bad syntax.")
+
+    def email_new(self):
+        if self.email and input("Sure?[Yes/No]") == "No":
+            print("Stop.")
+            return
+
+        from_addr = input("From email address:")
+        from_name = input("From name:")
+        subject = input("Title:")
+
+        self.email = Email((from_name, from_addr), subject)
+        print("Okay.")
+
+    def email_delete(self):
+        if input("Sure?[Yes/No]") == "No":
+            print("Stop.")
+            return
+
+        self.email = None
+        print("Okay.")
+
+    def email_text(self):
+        self.email.add_text(self.get_text())
+        print("Okay.")
+
+    def email_text_file(self):
+        try:
+            self.email.add_text_from_file(input("Path:"))
+        except FileNotFoundError:
+            print("File not fount.")
+        except IOError:
+            print("IO error.")
+        else:
+            print("Okay.")
+
+    def email_html(self):
+        self.email.add_html(self.get_text())
+        print("Okay.")
+
+    def email_html_file(self):
+        try:
+            self.email.add_html_from_file(input("Path:"))
+        except FileNotFoundError:
+            print("File not fount.")
+        except IOError:
+            print("IO error.")
+        else:
+            print("Okay.")
+
+    def email_file(self):
+        filename = input("Filename:")
+        path = input("Path:")
+        try:
+            self.email.add_from_file(filename, path)
+        except FileNotFoundError:
+            print("File not fount.")
+        except IOError:
+            print("IO error.")
+        else:
+            print("Okay.")
+
+    def email_show(self):
+        print(f"From: {self.email.from_addr[0]} <{self.email.from_addr[1]}>")
+        print(f"Title: {self.email.subject}")
+
+        print("* Text: ")
+        for i in self.email.text:
+            print(i)
+            print("-" * 5)
+
+        print("* HTML: ")
+        for i in self.email.html:
+            print(i)
+            print("-" * 5)
+
+        print("* File: ")
+        for i in self.email.file:
+            print(i[0])
+
+    @staticmethod
+    def get_text():
+        end = input("End:")
+        text = ""
+        while True:
+            get = input(">>> ")
+            if get == end:
+                return text[:-1]
+            text += get + "\n"
+
+    def do_rc(self, arg):
+        """rc add-to    --    add recipient.
+rc add-cc    --    add CC person.
+rc add-bcc   --    add BCC person.
+rc show      --    show recipient, CC person, BCC person."""
+
+        if not self.email:
+            print("Please create email first.")
+            return
+
+        if arg == "add-to":
+            self.rc_add_to()
+        elif arg == "add-cc":
+            self.rc_add_cc()
+        elif arg == "add-bcc":
+            self.rc_add_bcc()
+        elif arg == "show":
+            self.rc_show()
+        else:
+            print(f"Bad syntax.")
+
+    def rc_add_to(self):
+        name = input("Name:")
+        email = input("Email:")
+        self.email.add_to_addr(name, email)
+        print("Okay.")
+
+    def rc_add_cc(self):
+        name = input("Name:")
+        email = input("Email:")
+        self.email.add_cc_addr(name, email)
+        print("Okay.")
+
+    def rc_add_bcc(self):
+        name = input("Name:")
+        email = input("Email:")
+        self.email.add_bcc_addr(name, email)
+        print("Okay.")
+
+    def rc_show(self):
+        print("* Recipient:")
+        for name, email in self.email.to_addr:
+            print(f"{name} <{email}>")
+
+        print("* CC Person:")
+        for name, email in self.email.cc_addr:
+            print(f"{name} <{email}>")
+
+        print("* BCC Person:")
+        for name, email in self.email.bcc_addr:
+            print(f"{name} <{email}>")
+
+    def do_send(self, arg):
+        """Send email"""
+        if len(arg) != 0:
+            print(f"Bad syntax for '{arg}'.")
+            return
+
+        if not self.email:
+            print("Please create email first.")
+            return
+
+        if not self.sender:
+            print("Please create sender first.")
+            return
+
+        try:
+            self.sender.send(self.email)
+        except smtplib.SMTPException:
+            print("SMTP error.")
+        else:
+            print("Okay.")
+
+    def do_quit(self, _):
+        """Exit HuanMail."""
+
+        print("Bye~")
+        if self.file:
+            self.file.close()
+            self.file = None
+        return True
+
+
+if __name__ == '__main__':
+    manager = CLManager()
+    try:
+        manager.cmdloop()
+    except KeyboardInterrupt:
+        print("\nBye~")
+        quit(0)

+ 0 - 0
sender/__init__.py


+ 77 - 0
sender/email.py

@@ -0,0 +1,77 @@
+from time import strftime, localtime
+
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from email.mime.application import MIMEApplication
+from email.header import Header
+from email.utils import parseaddr, formataddr
+
+
+class Email:
+    @staticmethod
+    def _format_addr(s):
+        name, addr = parseaddr(s)
+        return formataddr((Header(name, 'utf-8').encode(), addr))
+
+    def __init__(self, from_addr: tuple, subject: str, to_addr=None, cc_addr=None, bcc_addr=None, subtype="mixed"):
+        self.from_addr = from_addr
+        self.subject = subject
+        self.to_addr = to_addr if to_addr else []
+        self.cc_addr = cc_addr if cc_addr else []
+        self.bcc_addr = bcc_addr if bcc_addr else []
+
+        self.text = []
+        self.html = []
+        self.file = []
+        self.subtype = subtype
+
+    def add_text(self, text: str):
+        self.text.append(text)
+
+    def add_text_from_file(self, path):
+        with open(path, "r") as f:
+            self.add_text(f.read())
+
+    def add_html(self, html: str):
+        self.html.append(html)
+
+    def add_html_from_file(self, path):
+        with open(path, "r") as f:
+            self.add_html(f.read())
+
+    def add_bytes(self, filename: str, file: bytes):
+        self.file.append((filename, file))
+
+    def add_from_file(self, filename, path):
+        with open(path, "rb") as f:
+            self.add_bytes(filename, f.read())
+
+    def add_to_addr(self, name, email):
+        self.to_addr.append((name, email))
+
+    def add_cc_addr(self, name, email):
+        self.cc_addr.append((name, email))
+
+    def add_bcc_addr(self, name, email):
+        self.bcc_addr.append((name, email))
+
+    def as_msg(self):
+        msg = MIMEMultipart(_subtype=self.subtype)
+        msg['From'] = self._format_addr(f"{self.from_addr[0]}<{self.from_addr[1]}>")
+        msg['To'] = ",".join([self._format_addr(f"{i[0]}<{i[1]}>") for i in self.to_addr])
+        msg['Cc'] = ",".join([self._format_addr(f"{i[0]}<{i[1]}>") for i in self.cc_addr])
+        msg['Subject'] = Header(self.subject, 'utf-8').encode()
+        msg["Date"] = Header(strftime('%a, %d %b %Y %H:%M:%S %z', localtime())).encode()
+
+        for i in self.text:
+            msg.attach(MIMEText(i, 'plain', 'utf-8'))
+        for i in self.html:
+            msg.attach(MIMEText(i, 'html', 'utf-8'))
+        for filename, i in self.file:
+            msg_file = MIMEApplication(i)
+            msg_file.add_header('Content-Disposition', 'attachment', filename=filename)
+            msg.attach(msg_file)
+        return msg
+
+    def as_string(self):
+        return self.as_msg().as_string()

+ 29 - 0
sender/smtp.py

@@ -0,0 +1,29 @@
+import smtplib
+
+from .email import Email
+
+
+class Sender:
+    def __init__(self, user: str, passwd: str, host="localhost", port=465, debug=False, ssl=True, start_ssl=False):
+        self.host = host
+        self.port = port
+        self.user = user
+        self.passwd = passwd
+        self.debug = debug
+        self.ssl = ssl
+        self.start_ssl = False if ssl else start_ssl
+
+    def send(self, msg: Email):
+        if self.ssl:
+            server = smtplib.SMTP_SSL(self.host, self.port)
+        else:
+            server = smtplib.SMTP(self.host, self.port)
+        server.set_debuglevel(self.debug)
+        if self.start_ssl:
+            server.starttls()
+        server.login(self.user, self.passwd)
+        server.sendmail(msg.from_addr[1],
+                             [i[1] for i in msg.to_addr + msg.cc_addr + msg.bcc_addr],
+                             msg.as_string())
+        server.quit()
+