Browse Source

feat: 下载邮件

SongZihuan 2 years ago
parent
commit
a7f31c3538
9 changed files with 298 additions and 13 deletions
  1. 16 9
      mailbox/email.py
  2. 6 0
      mailbox/imap.py
  3. 20 3
      templates/base.html
  4. 7 1
      templates/index/index.html
  5. 71 0
      templates/mailbox/mailbox.html
  6. 3 0
      web/__init__.py
  7. 74 0
      web/mailbox.py
  8. 44 0
      web/page.py
  9. 57 0
      web/user.py

+ 16 - 9
mailbox/email.py

@@ -19,21 +19,28 @@ class Mail:
     time_zone_pattern = re.compile(r"([+-])([0-9]{2})00")
     time_zone_pattern = re.compile(r"([+-])([0-9]{2})00")
 
 
     def __init__(self, num: str, data: bytes):
     def __init__(self, num: str, data: bytes):
-        self.__data = message_from_bytes(data)
+        self.__date_ = None
         self.byte = data
         self.byte = data
         self.num = num
         self.num = num
 
 
+    @property
+    def msg_data(self):  # 有需要的时候才加载
+        if self.__date_:
+            return self.__date_
+        self.__date_ = message_from_bytes(self.byte)
+        return self.__date_
+
     @property
     @property
     def from_addr(self):
     def from_addr(self):
-        if not self.__data['From']:
+        if not self.msg_data['From']:
             return ""
             return ""
-        return str(email.header.make_header(email.header.decode_header(self.__data['From'])))
+        return str(email.header.make_header(email.header.decode_header(self.msg_data['From'])))
 
 
     @property
     @property
     def date(self):
     def date(self):
-        if not self.__data['Date']:
+        if not self.msg_data['Date']:
             return datetime.datetime(2022, 1, 1)
             return datetime.datetime(2022, 1, 1)
-        date = str(email.header.make_header(email.header.decode_header(self.__data['Date'])))
+        date = str(email.header.make_header(email.header.decode_header(self.msg_data['Date'])))
         res = self.date_pattern.match(str(date)).groups()
         res = self.date_pattern.match(str(date)).groups()
         time = datetime.datetime(int(res[2]),
         time = datetime.datetime(int(res[2]),
                                  list(calendar.month_abbr).index(res[1]),
                                  list(calendar.month_abbr).index(res[1]),
@@ -53,15 +60,15 @@ class Mail:
 
 
     @property
     @property
     def title(self):
     def title(self):
-        if not self.__data['Subject']:
+        if not self.msg_data['Subject']:
             return ""
             return ""
-        return (str(email.header.make_header(email.header.decode_header(self.__data['Subject'])))
+        return (str(email.header.make_header(email.header.decode_header(self.msg_data['Subject'])))
                 .replace('\n', '')
                 .replace('\n', '')
                 .replace('\r', ''))
                 .replace('\r', ''))
 
 
     @property
     @property
     def body(self):
     def body(self):
-        return self.__get_body(self.__data)
+        return self.__get_body(self.msg_data)
 
 
     def __get_body(self, msg):
     def __get_body(self, msg):
         if msg.is_multipart():
         if msg.is_multipart():
@@ -79,7 +86,7 @@ class Mail:
                 return ""
                 return ""
 
 
     def save_file(self, file_dir: str):
     def save_file(self, file_dir: str):
-        return self.__get_files(self.__data, file_dir)
+        return self.__get_files(self.msg_data, file_dir)
 
 
     @staticmethod
     @staticmethod
     def __get_files(msg, file_dir: str):
     def __get_files(msg, file_dir: str):

+ 6 - 0
mailbox/imap.py

@@ -97,6 +97,12 @@ class Imap:
             self.__fetch(i)
             self.__fetch(i)
         self.disconnect()
         self.disconnect()
 
 
+    def fetch_remote_count(self, opt="ALL"):
+        self.connect()
+        res = len(set(self.__search(opt)) - set(self.__mailbox.keys()))
+        self.disconnect()
+        return res
+
     @property
     @property
     def mailbox(self) -> List[Mail]:
     def mailbox(self) -> List[Mail]:
         return sorted(self.__mailbox.values(), reverse=True)
         return sorted(self.__mailbox.values(), reverse=True)

+ 20 - 3
templates/base.html

@@ -1,3 +1,19 @@
+{% macro get_page_list(info_lines, now_page) %}
+    <ul class="pagination justify-content-center">
+        {% for line in info_lines %}
+            {% if line %}
+                {% if now_page == line[0] %}
+                    <li class="page-item active"><a class="page-link" href="{{ line[1] }}"> {{ line[0] }} </a></li>
+                {% else %}
+                    <li class="page-item"><a class="page-link" href="{{ line[1] }}"> {{ line[0] }} </a></li>
+                {% endif %}
+            {% else %}
+                <li class="page-item disabled"><a class="page-link"> ... </a></li>
+            {% endif %}
+        {% endfor %}
+    </ul>
+{% endmacro %}
+
 {% macro render_field(field) %}
 {% macro render_field(field) %}
     <div class="form-group form-floating my-3">
     <div class="form-group form-floating my-3">
         {% if not field.errors %}
         {% if not field.errors %}
@@ -71,10 +87,11 @@
 
 
 <body>
 <body>
     {% block nav %}
     {% block nav %}
-        <div class="container mt-2">
-            <a class="h3" href="/" style="text-decoration:none;color:#333;"> {{ conf["WEBSITE_TITLE"] }} </a>
-            <a href="{{ url_for("auth.logout_page") }}" class="btn btn-outline-danger float-end mx-2"> 退出登录 </a>
+        <div class="container mt-2 text-end">
+            <a class="h1 float-start" href="/" style="text-decoration:none;color:#333;"> {{ conf["WEBSITE_TITLE"] }} </a>
+            <a href="{{ url_for("auth.logout_page") }}" class="btn btn-outline-danger mx-2 my-1"> 退出登录 </a>
         </div>
         </div>
+        <hr>
     {% endblock %}
     {% endblock %}
 
 
     <section class="container mt-4 mb-2">
     <section class="container mt-4 mb-2">

+ 7 - 1
templates/index/index.html

@@ -3,6 +3,12 @@
 
 
 {% block content %}
 {% block content %}
 <div class="container text-center">
 <div class="container text-center">
-    <h5> 欢迎,{{ current_user.id }} </h5>
+    <h3> 欢迎,{{ current_user.id }} ! </h3>
+
+    <div class="btn-group mt-5">
+        <a class="btn btn-danger" href="{{ url_for("mailbox.mail_list_page") }}"> 查看邮箱 </a>
+        <a class="btn btn-success"> 发送邮件 </a>
+    </div>
+
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 71 - 0
templates/mailbox/mailbox.html

@@ -0,0 +1,71 @@
+{% extends "base.html" %}
+{% block title %} 首页 {% endblock %}
+
+{% block content %}
+<div class="container text-center">
+    {% if date %}
+        <h5> {{ date }} - {{ select }} </h5>
+    {% endif %}
+
+    <form method="get" action="{{ url_for("mailbox.mail_list_page") }}" class="was-validated">
+        {# 不需要隐藏字段 #}
+        {{ render_field(to_mail.date) }}
+        <div class="text-end">
+            {{ to_mail.submit(class='btn btn-success me-2') }}
+        </div>
+    </form>
+
+    {% if page == 1 %}
+        <div class="card mt-3">
+            <div class="card-header text-start"> 往后一天查看 </div>
+            <div class="card-body text-start">
+                跳转到后一天的邮箱查看邮件。
+                <div class="text-end">
+                    <a class="card-link" href="{{ url_for("mailbox.mail_list_page", date=next_date, select=select) }}"> 前往 </a>
+                </div>
+            </div>
+        </div>
+    {% endif %}
+
+    {% if empty %}
+        <div class="card mt-3">
+            <div class="card-header text-start bg-warning"> 未加载出任何邮件 </div>
+            <div class="card-body text-start"> 尝试往前或往后查看,或者等待一段时间吧!</div>
+        </div>
+    {% endif %}
+
+    {% for i in mail_list %}
+        <div class="card mt-3">
+            <div class="card-header text-start"> {{ i.title }} </div>
+            <div class="card-body text-start">
+                <p>
+                    Date: {{ i.date }}
+                    <br>
+                    From: {{ i.from_addr }}
+                </p>
+
+                <div class="text-end">
+                    <a class="card-link"> 查看 </a>
+                </div>
+            </div>
+        </div>
+    {% endfor %}
+
+    {% if page == max_page %}
+        <div class="card mt-3">
+            <div class="card-header text-start"> 往前一天查看 </div>
+            <div class="card-body text-start">
+                跳转到前一天的邮箱查看邮件。
+                <div class="text-end">
+                    <a class="card-link" href="{{ url_for("mailbox.mail_list_page", date=last_date, select=select) }}"> 前往 </a>
+                </div>
+            </div>
+        </div>
+    {% endif %}
+
+    <div class="mt-3">
+        {{ get_page_list(page_list, page) }}
+    </div>
+
+</div>
+{% endblock %}

+ 3 - 0
web/__init__.py

@@ -38,6 +38,9 @@ class HuamMailFlask(Flask):
         from .auth import auth
         from .auth import auth
         self.register_blueprint(auth, url_prefix="/auth")
         self.register_blueprint(auth, url_prefix="/auth")
 
 
+        from .mailbox import mailbox
+        self.register_blueprint(mailbox, url_prefix="/mailbox")
+
     def profile_setting(self):
     def profile_setting(self):
         if conf["DEBUG_PROFILE"]:
         if conf["DEBUG_PROFILE"]:
             self.wsgi_app = ProfilerMiddleware(self.wsgi_app, sort_by=("cumtime",))
             self.wsgi_app = ProfilerMiddleware(self.wsgi_app, sort_by=("cumtime",))

+ 74 - 0
web/mailbox.py

@@ -0,0 +1,74 @@
+from flask import Blueprint, render_template, request, flash
+from flask_login import login_required, current_user
+from flask_wtf import FlaskForm
+from wtforms import DateField, SubmitField
+from wtforms.validators import DataRequired
+from time import strftime, strptime, mktime, localtime
+from typing import List
+
+from mailbox.email import Mail
+from .page import get_page, get_max_page
+from .logger import Logger
+
+
+mailbox = Blueprint("mailbox", __name__)
+
+
+class ToMailboxForm(FlaskForm):
+    date = DateField("发信时间", description="信件发送时间", validators=[DataRequired("必须选择时间")])
+    submit = SubmitField("查询")
+
+
+
+def __load_mailbox_page(mail_list, page, to_mail=None, date=None, select=None, next_date=None, last_date=None):
+    if not to_mail:
+        to_mail = ToMailboxForm()
+
+    max_page = get_max_page(len(mail_list), 10)
+    page_list = get_page("mailbox.mail_list_page", page, max_page, date=date, select=select)
+    page_mail_list: List[Mail] = mail_list[(page - 1) * 10: page * 10]
+
+    return render_template("mailbox/mailbox.html",
+                           to_mail=to_mail,
+                           date=date,
+                           select=select,
+                           page_list=page_list,
+                           page=page,
+                           mail_list=page_mail_list,
+                           max_page=max_page,
+                           empty=(len(page_mail_list) == 0),
+                           next_date=next_date,
+                           last_date=last_date)
+
+
+@mailbox.route("/")
+@login_required
+def mail_list_page():
+    date = request.args.get("date", None, type=str)
+    select = request.args.get("select", "INBOX", type=str)
+    page = request.args.get("page", 1, type=int)
+
+    if date:
+        date_obj = strptime(date, "%Y-%m-%d")
+        mail_list, download = current_user.get_mail(select, strftime('%d-%b-%Y', date_obj))
+        if mail_list is None and not download:
+            flash("旧任务未完成,正在下载邮件,请稍后")
+            mail_list = []
+        elif download:
+            flash("启动新任务下载邮件,请稍后")
+            mail_list = []
+
+        next_date = strftime("%Y-%m-%d", localtime(mktime(date_obj) + 24 * 60 * 60))
+        last_date = strftime("%Y-%m-%d", localtime(mktime(date_obj) - 24 * 60 * 60))
+    else:
+        mail_list = []
+        next_date = None
+        last_date = None
+
+    Logger.print_load_page_log("mail list")
+    return __load_mailbox_page(mail_list,
+                               page,
+                               date=date,
+                               select=select,
+                               next_date=next_date,
+                               last_date=last_date)

+ 44 - 0
web/page.py

@@ -0,0 +1,44 @@
+from flask import url_for
+from typing import Optional
+
+
+def get_max_page(count: int, count_page: int):
+    """ 计算页码数 (共计count个元素, 每页count_page个元素) """
+    res = (count // count_page) + (0 if count % count_page == 0 else 1)
+    if res == 0:
+        return 1
+    return res
+
+
+def get_page(url, page: int, count: int, **kwargs):
+    """ 计算页码的按钮 """
+    if count <= 9:
+        page_list = [[i + 1, url_for(url, page=i + 1, **kwargs)] for i in range(count)]
+    elif page <= 5:
+        """
+        [1][2][3][4][5][6][...][count - 1][count]
+        """
+        page_list = [[i + 1, url_for(url, page=i + 1)] for i in range(6)]
+        page_list += [None,
+                      [count - 1, url_for(url, page=count - 1, **kwargs)],
+                      [count, url_for(url, page=count, **kwargs)]]
+    elif page >= count - 5:
+        """
+        [1][2][...][count - 5][count - 4][count - 3][count - 2][count - 1][count]
+        """
+        page_list: Optional[list] = [[1, url_for(url, page=1, **kwargs)],
+                                     [2, url_for(url, page=2, **kwargs)],
+                                     None]
+        page_list += [[count - 5 + i, url_for(url, page=count - 5 + i, **kwargs)] for i in range(6)]
+    else:
+        """
+        [1][2][...][page - 2][page - 1][page][page + 1][page + 2][...][count - 1][count]
+        """
+        page_list: Optional[list] = [[1, url_for(url, page=1, **kwargs)],
+                                     [2, url_for(url, page=2, **kwargs)],
+                                     None]
+        page_list += [[page - 2 + i, url_for(url, page=page - 2 + i, **kwargs)] for i in range(5)]
+        page_list += [None,
+                      [count - 1, url_for(url, page=count - 1, **kwargs)],
+                      [count, url_for(url, page=count, **kwargs)]]
+    return page_list

+ 57 - 0
web/user.py

@@ -4,6 +4,7 @@ from .db import redis
 from .configure import conf
 from .configure import conf
 
 
 from flask_login import UserMixin
 from flask_login import UserMixin
+from threading import Thread
 
 
 
 
 class User(UserMixin):
 class User(UserMixin):
@@ -41,3 +42,59 @@ class User(UserMixin):
     @property
     @property
     def passwd(self):
     def passwd(self):
         return self.info.get("passwd", "123456789")
         return self.info.get("passwd", "123456789")
+
+    class DownloadMail(Thread):
+        def __init__(self, username, passwd, inbox, date):
+            super(User.DownloadMail, self).__init__()
+
+            self.imap = Imap(user=conf["IMAP_USERNAME"].format(username),
+                             passwd=conf["IMAP_PASSWD"].format(passwd),
+                             host=conf["IMAP_HOST"],
+                             port=conf["IMAP_PORT"],
+                             ssl=conf["IMAP_SSL"],
+                             start_ssl=conf["IMAP_START_SSL"])
+            self.username = username
+            self.imap.inbox = inbox
+            self.date = date
+            self.inbox = inbox
+
+        def run(self):
+            try:
+                for i in redis.keys(f"mailbox:{self.username}:{self.inbox}:{self.date}:*"):
+                    num = i.split(":")[-1]
+                    byte = redis.get(i)
+                    self.imap.add_mail(num, byte.encode("utf-8"))
+
+                self.imap.fetch_all(f"ON {self.date}")
+
+                for i in self.imap.mailbox:
+                    try:
+                        redis.set(f"mailbox:{self.username}:{self.inbox}:{self.date}:{i.num}", i.byte)
+                    except UnicodeDecodeError:
+                        redis.set(f"mailbox:{self.username}:{self.inbox}:{self.date}:{i.num}", b"")
+            finally:
+                redis.set(f"download:mutex:{self.username}", 0)
+
+    def get_mail(self, inbox: str, date: str):
+        imap = Imap(user=conf["IMAP_USERNAME"].format(self.username),
+                    passwd=conf["IMAP_PASSWD"].format(self.passwd),
+                    host=conf["IMAP_HOST"],
+                    port=conf["IMAP_PORT"],
+                    ssl=conf["IMAP_SSL"],
+                    start_ssl=conf["IMAP_START_SSL"])
+        imap.inbox = inbox
+
+        for i in redis.keys(f"mailbox:{self.username}:{inbox}:{date}:*"):
+            num = i.split(":")[-1]
+            byte = redis.get(i)
+            imap.add_mail(num, byte.encode("utf-8"))
+
+        if imap.fetch_remote_count(f"ON {date}") != 0:
+            # 需要从远程服务器下载资源
+            res = redis.incr(f"download:mutex:{self.username}")
+            if res != 1:
+                return None, False  # 已经有线程
+            th = User.DownloadMail(self.username, self.passwd, inbox, date)
+            th.start()
+            return imap.mailbox, True
+        return imap.mailbox, False