diff --git a/.gitignore b/.gitignore index 68bc17f..677ae5b 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,6 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ +config.ini +*.session* diff --git a/config.gen.ini b/config.gen.ini new file mode 100644 index 0000000..66e44da --- /dev/null +++ b/config.gen.ini @@ -0,0 +1,13 @@ +[pyrogram] +api_id = 143461 +api_hash = 7b8a66cb31224f4241102d7fc57b5bcd + +[basic] +ipv6 = False +cache_uri = mem:// + +[imap] +host = xxx +username = xxx +password = xxx +chat_id = 1111 diff --git a/defs/models.py b/defs/models.py new file mode 100644 index 0000000..1b8001d --- /dev/null +++ b/defs/models.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pyrogram import Client + from pyrogram.types import Message + +TEMP = """#mail +✉️ %s (%s) +To: %s + +%s""" + + +class Mail(BaseModel): + id: int + from_: str + subject: str + to: str + + @property + def from_name(self) -> str: + li = self.from_.split(" <") + return " <".join(li[:-1]).strip() + + @property + def from_at(self) -> str: + li = self.from_.split(" <") + return li[-1].strip()[:-1] + + @property + def text(self) -> str: + return TEMP % (self.from_name, self.from_at, self.to, self.subject) + + async def send(self, bot: "Client", chat_id: int) -> "Message": + return await bot.send_message(chat_id, self.text) diff --git a/defs/search.py b/defs/search.py new file mode 100644 index 0000000..798e71c --- /dev/null +++ b/defs/search.py @@ -0,0 +1,54 @@ +import email.header +from datetime import date, datetime, timedelta +from typing import List + +from cashews import cache +from imapclient import IMAPClient + +from defs.models import Mail +from glover import host, days, username, password + + +def decode_mime_words(s): + return u''.join( + word.decode(encoding or 'utf8') if isinstance(word, bytes) else word + for word, encoding in email.header.decode_header(s)) + + +def get_date() -> date: + now = datetime.now() + old = now - timedelta(days=days) + return date(old.year, old.month, old.day) + + +async def filter_mail(ids: List[int]) -> List[int]: + new = [] + for mid in ids: + if await cache.get(f"mail:{username}:{mid}"): + continue + new.append(mid) + return new + + +async def search() -> List[Mail]: + with IMAPClient(host=host) as client: + client.login(username, password) + client.select_folder("INBOX") + + messages = client.search([u'SINCE', get_date()]) + messages = await filter_mail(messages) + response = client.fetch(messages, ["RFC822"]) + mails = [] + for message_id, data in response.items(): + email_message = email.message_from_bytes(data[b"RFC822"]) + from_ = email_message.get("From") + subject = decode_mime_words(email_message.get("Subject")) + to = email_message.get("To") + mail = Mail( + id=message_id, + from_=from_, + subject=subject, + to=to, + ) + mails.append(mail) + return mails diff --git a/glover.py b/glover.py new file mode 100644 index 0000000..f6d2c10 --- /dev/null +++ b/glover.py @@ -0,0 +1,47 @@ +from typing import Union +from configparser import RawConfigParser + + +def strtobool(val, default=False): + """Convert a string representation of truth to true (1) or false (0). + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + if val is None: + return default + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return 1 + elif val in ("n", "no", "f", "false", "off", "0"): + return 0 + else: + print("[Degrade] invalid truth value %r" % (val,)) + return default + + +# [pyrogram] +api_id: int = 0 +api_hash: str = "" +# [Basic] +ipv6: Union[bool, str] = "False" +cache_uri: str = "mem://" +# [Imap] +host: str = "" +username: str = "" +password: str = "" +days: int = 3 +chat_id: int = 0 + +config = RawConfigParser() +config.read("config.ini") +api_id = config.getint("pyrogram", "api_id", fallback=api_id) +api_hash = config.get("pyrogram", "api_hash", fallback=api_hash) +ipv6 = strtobool(config.get("basic", "ipv6", fallback=ipv6)) +cache_uri = config.get("basic", "cache_uri", fallback=cache_uri) +host = config.get("imap", "host", fallback=host) +username = config.get("imap", "username", fallback=username) +password = config.get("imap", "password", fallback=password) +days = config.getint("imap", "days", fallback=days) +chat_id = config.getint("imap", "chat_id", fallback=chat_id) diff --git a/init.py b/init.py new file mode 100644 index 0000000..89d39ec --- /dev/null +++ b/init.py @@ -0,0 +1,33 @@ +import pyrogram +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from cashews import cache + +from glover import api_id, api_hash, ipv6, cache_uri +from logging import getLogger, INFO, StreamHandler, basicConfig, CRITICAL, Formatter + +# Set Cache +cache.setup(cache_uri) +# Enable logging +logs = getLogger(__name__) +logging_handler = StreamHandler() +dt_fmt = "%Y-%m-%d %H:%M:%S" +formatter = Formatter( + "[{asctime}] [{levelname:<8}] {name}: {message}", dt_fmt, style="{" +) +logging_handler.setFormatter(formatter) +root_logger = getLogger() +root_logger.setLevel(CRITICAL) +root_logger.addHandler(logging_handler) +pyro_logger = getLogger("pyrogram") +pyro_logger.setLevel(CRITICAL) +pyro_logger.addHandler(logging_handler) +basicConfig(level=INFO) +logs.setLevel(INFO) + +scheduler = AsyncIOScheduler(timezone="Asia/ShangHai") +if not scheduler.running: + scheduler.start() +# Init client +bot = pyrogram.Client( + "bot", api_id=api_id, api_hash=api_hash, ipv6=ipv6, plugins=dict(root="modules") +) diff --git a/main.py b/main.py new file mode 100644 index 0000000..3b50e12 --- /dev/null +++ b/main.py @@ -0,0 +1,10 @@ +from pyrogram import idle + +from init import bot, logs + +if __name__ == "__main__": + logs.info("Bot 开始运行") + bot.start() + logs.info(f"Bot 启动成功!@{bot.me.username}") + idle() + bot.stop() diff --git a/modules/ping.py b/modules/ping.py new file mode 100644 index 0000000..291d90e --- /dev/null +++ b/modules/ping.py @@ -0,0 +1,7 @@ +from pyrogram import Client, filters +from pyrogram.types import Message + + +@Client.on_message(filters.incoming & filters.private & filters.command(["ping"])) +async def ping_command(_: Client, message: Message): + await message.reply("pong~", quote=True) diff --git a/modules/update.py b/modules/update.py new file mode 100644 index 0000000..21d5900 --- /dev/null +++ b/modules/update.py @@ -0,0 +1,22 @@ +from cashews import cache + +from glover import chat_id, username +from init import scheduler, bot, logs +from defs.search import search + + +# 15 分钟执行一次 +@scheduler.scheduled_job("interval", minutes=15) +async def update(): + logs.info("开始检查新邮件") + mails = await search() + for m in mails: + try: + await m.send(bot, chat_id) + await cache.set(f"mail:{username}:{m.id}", 1) + except Exception as e: + logs.exception("发送邮件失败", exc_info=e) + logs.info("检查新邮件结束") + + +bot.loop.create_task(update()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7e7b5d0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +imapclient +apscheduler +pyrogram +tgcrypto +pydantic +cashews[redis]