download-flow-enmu 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. download_flow_enmu-0.1.0/PKG-INFO +92 -0
  2. download_flow_enmu-0.1.0/README.md +72 -0
  3. download_flow_enmu-0.1.0/cli/__init__.py +1 -0
  4. download_flow_enmu-0.1.0/cli/download_cmd.py +64 -0
  5. download_flow_enmu-0.1.0/cli/main.py +11 -0
  6. download_flow_enmu-0.1.0/cli/root.py +25 -0
  7. download_flow_enmu-0.1.0/config/__init__.py +0 -0
  8. download_flow_enmu-0.1.0/config/loader.py +58 -0
  9. download_flow_enmu-0.1.0/data/bill_profile.py +39 -0
  10. download_flow_enmu-0.1.0/download_flow_enmu.egg-info/PKG-INFO +92 -0
  11. download_flow_enmu-0.1.0/download_flow_enmu.egg-info/SOURCES.txt +28 -0
  12. download_flow_enmu-0.1.0/download_flow_enmu.egg-info/dependency_links.txt +1 -0
  13. download_flow_enmu-0.1.0/download_flow_enmu.egg-info/entry_points.txt +2 -0
  14. download_flow_enmu-0.1.0/download_flow_enmu.egg-info/requires.txt +7 -0
  15. download_flow_enmu-0.1.0/download_flow_enmu.egg-info/top_level.txt +6 -0
  16. download_flow_enmu-0.1.0/factory/downloader_factory.py +8 -0
  17. download_flow_enmu-0.1.0/factory/email_factory.py +11 -0
  18. download_flow_enmu-0.1.0/factory/provider_factory.py +9 -0
  19. download_flow_enmu-0.1.0/package/__init__.py +0 -0
  20. download_flow_enmu-0.1.0/package/downfile.py +192 -0
  21. download_flow_enmu-0.1.0/pyproject.toml +41 -0
  22. download_flow_enmu-0.1.0/setup.cfg +4 -0
  23. download_flow_enmu-0.1.0/strategy/downloader/__init__.py +0 -0
  24. download_flow_enmu-0.1.0/strategy/downloader/attchment.py +41 -0
  25. download_flow_enmu-0.1.0/strategy/downloader/provider.py +9 -0
  26. download_flow_enmu-0.1.0/strategy/email/__init__.py +0 -0
  27. download_flow_enmu-0.1.0/strategy/email/email_provider.py +9 -0
  28. download_flow_enmu-0.1.0/strategy/email/gmail.py +19 -0
  29. download_flow_enmu-0.1.0/strategy/provider/ali_provider.py +31 -0
  30. download_flow_enmu-0.1.0/strategy/provider/provider.py +8 -0
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: download-flow-enmu
3
+ Version: 0.1.0
4
+ Summary: A task flow management tool for syncing todo files with taskwarrior
5
+ Author-email: Your Name <your.email@example.com>
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: typer>=0.9.0
15
+ Requires-Dist: pydantic>=2.0.0
16
+ Requires-Dist: imap_tools
17
+ Requires-Dist: python-dotenv>=1.0.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
20
+
21
+ # tflow
22
+
23
+ A task flow management tool for syncing todo files with taskwarrior.
24
+
25
+ ## Installation
26
+
27
+ ### Development Installation (Editable Mode)
28
+
29
+ Install the package in editable mode so you can use the `tflow` command anywhere:
30
+
31
+ ```bash
32
+ # In the project directory
33
+ pip install -e .
34
+ ```
35
+
36
+ ### Regular Installation
37
+
38
+ ```bash
39
+ pip install .
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ After installation, you can use the `tflow` command directly:
45
+
46
+ ```bash
47
+ # Show available commands
48
+ tflow --help
49
+
50
+ # Read and import tasks from todo files
51
+ tflow read -c /path/to/files.yaml
52
+
53
+ # Modify tasks
54
+ tflow modfiy -a "new_data" -c "command"
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ Create a `files.yaml` configuration file to specify your todo file locations:
60
+
61
+ ```yaml
62
+ sources:
63
+ - id: "project1"
64
+ type: "static"
65
+ path:
66
+ darwin: "/path/to/project1.todo"
67
+ wsl: "/mnt/path/to/project1.todo"
68
+ - id: "daily"
69
+ type: "daily"
70
+ path:
71
+ darwin: "/path/to/agenda/todo"
72
+ wsl: "/mnt/path/to/agenda/todo"
73
+ ```
74
+
75
+ ## Development
76
+
77
+ ```bash
78
+ # Create virtual environment
79
+ python3 -m venv .venv
80
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
81
+
82
+ # Install dependencies
83
+ pip install -e ".[dev]"
84
+
85
+ # Run tests
86
+ pytest
87
+ ```
88
+
89
+ ## Requirements
90
+
91
+ - Python >= 3.10
92
+ - taskwarrior installed on your system
@@ -0,0 +1,72 @@
1
+ # tflow
2
+
3
+ A task flow management tool for syncing todo files with taskwarrior.
4
+
5
+ ## Installation
6
+
7
+ ### Development Installation (Editable Mode)
8
+
9
+ Install the package in editable mode so you can use the `tflow` command anywhere:
10
+
11
+ ```bash
12
+ # In the project directory
13
+ pip install -e .
14
+ ```
15
+
16
+ ### Regular Installation
17
+
18
+ ```bash
19
+ pip install .
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ After installation, you can use the `tflow` command directly:
25
+
26
+ ```bash
27
+ # Show available commands
28
+ tflow --help
29
+
30
+ # Read and import tasks from todo files
31
+ tflow read -c /path/to/files.yaml
32
+
33
+ # Modify tasks
34
+ tflow modfiy -a "new_data" -c "command"
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ Create a `files.yaml` configuration file to specify your todo file locations:
40
+
41
+ ```yaml
42
+ sources:
43
+ - id: "project1"
44
+ type: "static"
45
+ path:
46
+ darwin: "/path/to/project1.todo"
47
+ wsl: "/mnt/path/to/project1.todo"
48
+ - id: "daily"
49
+ type: "daily"
50
+ path:
51
+ darwin: "/path/to/agenda/todo"
52
+ wsl: "/mnt/path/to/agenda/todo"
53
+ ```
54
+
55
+ ## Development
56
+
57
+ ```bash
58
+ # Create virtual environment
59
+ python3 -m venv .venv
60
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
61
+
62
+ # Install dependencies
63
+ pip install -e ".[dev]"
64
+
65
+ # Run tests
66
+ pytest
67
+ ```
68
+
69
+ ## Requirements
70
+
71
+ - Python >= 3.10
72
+ - taskwarrior installed on your system
@@ -0,0 +1 @@
1
+ # cmd package
@@ -0,0 +1,64 @@
1
+ import typer
2
+ import json
3
+ import logging
4
+ from pathlib import Path
5
+ from pydantic import TypeAdapter
6
+ from factory import downloader_factory, email_factory, provider_factory
7
+ from data.bill_profile import PayConfig
8
+ from typing_extensions import Annotated
9
+ from cli.root import app
10
+
11
+
12
+ def run(provider: str):
13
+ # 配置日志
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format='%(levelname)s: %(message)s'
17
+ )
18
+
19
+ config_path = Path.home() / ".flow" / "config.json"
20
+
21
+ logging.info(f"开始下载 {provider} 账单...")
22
+ logging.info(f"配置文件路径: {config_path}")
23
+
24
+ try:
25
+ # 使用 ConfigLoader 加载配置(支持环境变量替换)
26
+ from config.loader import load_config
27
+ config_json = load_config(config_path)
28
+
29
+ logging.info(f"配置加载成功")
30
+
31
+ adapter = TypeAdapter(PayConfig)
32
+ config_obj = adapter.validate_python(config_json[provider])
33
+ downloader = downloader_factory.creat_downloader(config_obj.download)
34
+ email = email_factory.create_emial(
35
+ config_obj.email, config_obj.auth.model_dump(), config_obj.auth_type.value
36
+ )
37
+
38
+ provider_obj = provider_factory.create_workder(provider)
39
+ provider_obj.email = email
40
+ provider_obj.downloader = downloader
41
+ provider_obj.bill = config_obj.profile
42
+
43
+ logging.info(f"开始执行下载...")
44
+ provider_obj.process_bills()
45
+ logging.info(f"✅ 下载完成!")
46
+
47
+ except FileNotFoundError as e:
48
+ logging.error(f"配置文件未找到: {config_path}")
49
+ raise
50
+ except KeyError as e:
51
+ logging.error(f"配置中缺少 provider: {provider}")
52
+ raise
53
+ except Exception as e:
54
+ logging.error(f"处理失败: {e}")
55
+ raise
56
+
57
+
58
+ @app.command()
59
+ def download(
60
+ provider: Annotated[
61
+ str, typer.Option("--provider", "-p", help="config file")
62
+ ] = "alipay",
63
+ ):
64
+ run(provider)
@@ -0,0 +1,11 @@
1
+ from .root import app
2
+
3
+ from cli.download_cmd import download
4
+
5
+
6
+ def main():
7
+ app()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,25 @@
1
+ import typer
2
+
3
+ app = typer.Typer(help="demail")
4
+
5
+
6
+ def version_callback(value: bool):
7
+ if value:
8
+ print("Awesome CLI Version: 1.0.0")
9
+ raise typer.Exit()
10
+
11
+
12
+ @app.callback()
13
+ def main(
14
+ version: bool = typer.Option(
15
+ None,
16
+ "--version",
17
+ "-v",
18
+ help="Show version",
19
+ callback=version_callback,
20
+ is_eager=True,
21
+ ),
22
+ verbose: bool = typer.Option(False, "--verbose", help="Enable verbose output"),
23
+ ):
24
+ if verbose:
25
+ print("Verbose mode is ON")
File without changes
@@ -0,0 +1,58 @@
1
+ """
2
+ 配置加载工具,支持从环境变量替换配置值
3
+ """
4
+ import os
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any, Dict
9
+ from dotenv import load_dotenv
10
+
11
+
12
+ class ConfigLoader:
13
+ """配置加载器,支持环境变量替换"""
14
+
15
+ def __init__(self, env_file: Path = None):
16
+ if env_file is None:
17
+ # 默认在项目根目录查找 .env
18
+ env_file = Path(__file__).parent.parent / ".env"
19
+
20
+ # 加载 .env 文件
21
+ if env_file.exists():
22
+ load_dotenv(env_file)
23
+
24
+ def _replace_env_vars(self, value: Any) -> Any:
25
+ if isinstance(value, str):
26
+ # 匹配 ${VAR_NAME} 或 ${VAR_NAME:default}
27
+ pattern = r'\$\{([^}:]+)(?::([^}]*))?\}'
28
+
29
+ def replacer(match):
30
+ var_name = match.group(1)
31
+ default_value = match.group(2) if match.group(2) is not None else ""
32
+ return os.getenv(var_name, default_value)
33
+
34
+ return re.sub(pattern, replacer, value)
35
+
36
+ elif isinstance(value, dict):
37
+ return {k: self._replace_env_vars(v) for k, v in value.items()}
38
+
39
+ elif isinstance(value, list):
40
+ return [self._replace_env_vars(item) for item in value]
41
+
42
+ else:
43
+ return value
44
+
45
+ def load_json(self, config_path: Path) -> Dict[str, Any]:
46
+ with open(config_path, 'r', encoding='utf-8') as f:
47
+ config = json.load(f)
48
+
49
+ return self._replace_env_vars(config)
50
+
51
+
52
+ # 便捷函数
53
+ def load_config(config_path: Path = None) -> Dict[str, Any]:
54
+ if config_path is None:
55
+ config_path = Path.home() / ".flow" / "config.json"
56
+
57
+ loader = ConfigLoader()
58
+ return loader.load_json(config_path)
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from enum import Enum
4
+ from pydantic import BaseModel, model_validator
5
+
6
+
7
+ class BillProfile(BaseModel):
8
+ name: str = ""
9
+ search_subject: str = ""
10
+ sender_email: str = ""
11
+ file_suffix: str = ""
12
+ encoding: str = "utf-8"
13
+ save_subdir: Path = Path.home() / ".flow" / "data"
14
+
15
+ @model_validator(mode="after")
16
+ def set_dynamic_path(self):
17
+ if self.name:
18
+ self.save_subdir = self.save_subdir / self.name
19
+ return self
20
+
21
+
22
+ class AuthType(Enum):
23
+ PASS = "pass"
24
+ TOKEN = "token"
25
+
26
+
27
+ @dataclass
28
+ class Auth(BaseModel):
29
+ username: str = ""
30
+ password: str = ""
31
+ token: str = ""
32
+
33
+
34
+ class PayConfig(BaseModel):
35
+ email: str
36
+ download: str
37
+ auth_type: AuthType
38
+ auth: Auth
39
+ profile: BillProfile
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: download-flow-enmu
3
+ Version: 0.1.0
4
+ Summary: A task flow management tool for syncing todo files with taskwarrior
5
+ Author-email: Your Name <your.email@example.com>
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: typer>=0.9.0
15
+ Requires-Dist: pydantic>=2.0.0
16
+ Requires-Dist: imap_tools
17
+ Requires-Dist: python-dotenv>=1.0.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
20
+
21
+ # tflow
22
+
23
+ A task flow management tool for syncing todo files with taskwarrior.
24
+
25
+ ## Installation
26
+
27
+ ### Development Installation (Editable Mode)
28
+
29
+ Install the package in editable mode so you can use the `tflow` command anywhere:
30
+
31
+ ```bash
32
+ # In the project directory
33
+ pip install -e .
34
+ ```
35
+
36
+ ### Regular Installation
37
+
38
+ ```bash
39
+ pip install .
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ After installation, you can use the `tflow` command directly:
45
+
46
+ ```bash
47
+ # Show available commands
48
+ tflow --help
49
+
50
+ # Read and import tasks from todo files
51
+ tflow read -c /path/to/files.yaml
52
+
53
+ # Modify tasks
54
+ tflow modfiy -a "new_data" -c "command"
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ Create a `files.yaml` configuration file to specify your todo file locations:
60
+
61
+ ```yaml
62
+ sources:
63
+ - id: "project1"
64
+ type: "static"
65
+ path:
66
+ darwin: "/path/to/project1.todo"
67
+ wsl: "/mnt/path/to/project1.todo"
68
+ - id: "daily"
69
+ type: "daily"
70
+ path:
71
+ darwin: "/path/to/agenda/todo"
72
+ wsl: "/mnt/path/to/agenda/todo"
73
+ ```
74
+
75
+ ## Development
76
+
77
+ ```bash
78
+ # Create virtual environment
79
+ python3 -m venv .venv
80
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
81
+
82
+ # Install dependencies
83
+ pip install -e ".[dev]"
84
+
85
+ # Run tests
86
+ pytest
87
+ ```
88
+
89
+ ## Requirements
90
+
91
+ - Python >= 3.10
92
+ - taskwarrior installed on your system
@@ -0,0 +1,28 @@
1
+ README.md
2
+ pyproject.toml
3
+ cli/__init__.py
4
+ cli/download_cmd.py
5
+ cli/main.py
6
+ cli/root.py
7
+ config/__init__.py
8
+ config/loader.py
9
+ data/bill_profile.py
10
+ download_flow_enmu.egg-info/PKG-INFO
11
+ download_flow_enmu.egg-info/SOURCES.txt
12
+ download_flow_enmu.egg-info/dependency_links.txt
13
+ download_flow_enmu.egg-info/entry_points.txt
14
+ download_flow_enmu.egg-info/requires.txt
15
+ download_flow_enmu.egg-info/top_level.txt
16
+ factory/downloader_factory.py
17
+ factory/email_factory.py
18
+ factory/provider_factory.py
19
+ package/__init__.py
20
+ package/downfile.py
21
+ strategy/downloader/__init__.py
22
+ strategy/downloader/attchment.py
23
+ strategy/downloader/provider.py
24
+ strategy/email/__init__.py
25
+ strategy/email/email_provider.py
26
+ strategy/email/gmail.py
27
+ strategy/provider/ali_provider.py
28
+ strategy/provider/provider.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dflow = cli.main:main
@@ -0,0 +1,7 @@
1
+ typer>=0.9.0
2
+ pydantic>=2.0.0
3
+ imap_tools
4
+ python-dotenv>=1.0.0
5
+
6
+ [dev]
7
+ pytest>=7.0.0
@@ -0,0 +1,6 @@
1
+ cli
2
+ config
3
+ data
4
+ factory
5
+ package
6
+ strategy
@@ -0,0 +1,8 @@
1
+ from strategy.downloader.attchment import AttchmentDownloader
2
+
3
+
4
+ def creat_downloader(download_type: str):
5
+ if download_type == "attchment":
6
+ return AttchmentDownloader()
7
+ else:
8
+ raise NameError("未知下载器类型")
@@ -0,0 +1,11 @@
1
+ from strategy.email.gmail import Gmail
2
+
3
+
4
+ def create_emial(email: str, auth_config: dict, auth_type: str):
5
+ if email == "gmail" and auth_type == "pass":
6
+ return Gmail(
7
+ username=auth_config.get("username", ""),
8
+ password=auth_config.get("password", ""),
9
+ )
10
+ else:
11
+ raise NameError("未知邮箱")
@@ -0,0 +1,9 @@
1
+ from strategy.provider.ali_provider import AliProvider
2
+ from strategy.provider.ali_provider import AliProvider
3
+
4
+
5
+ def create_workder(provider: str):
6
+ if provider == "alipay":
7
+ return AliProvider()
8
+ else:
9
+ raise ValueError("未知供应商")
File without changes
@@ -0,0 +1,192 @@
1
+ import imaplib
2
+ import email
3
+ import logging
4
+ import os
5
+ import datetime
6
+ import re
7
+ from email.header import decode_header
8
+ import requests
9
+ from bs4 import BeautifulSoup
10
+ from config import config
11
+
12
+ # === 配置区域 ===
13
+ username = config["email"]["username"]
14
+ password = config["email"]["password"]
15
+ DOWNLOAD_FOLDER_ALI = config["email"]["download_folder_ali"]
16
+ DOWNLOAD_FOLDER_WECHAT = config["email"]["download_folder_wechat"]
17
+ SEARCH_KEYWORD = config["email"]["search_keyword"]
18
+ IMAP_SERVER = config["email"].get("imap_server", "imap.gmail.com")
19
+
20
+
21
+ def clean_text(text):
22
+ if not text:
23
+ return ""
24
+ decoded_list = decode_header(text)
25
+ text_str = ""
26
+ for decoded_part, encoding in decoded_list:
27
+ if isinstance(decoded_part, bytes):
28
+ text_str += decoded_part.decode(
29
+ encoding if encoding else "utf-8", errors="ignore"
30
+ )
31
+ else:
32
+ text_str += str(decoded_part)
33
+ return text_str
34
+
35
+
36
+ def get_html_content(msg):
37
+ for part in msg.walk():
38
+ # 只要 HTML 格式的部分
39
+ if part.get_content_type() == "text/html":
40
+ try:
41
+ payload = part.get_payload(decode=True)
42
+ charset = part.get_content_charset() or "utf-8"
43
+ return payload.decode(charset, errors="ignore")
44
+ except Exception as e:
45
+ logging.error(f"HTML解码失败: {e}")
46
+ return None
47
+
48
+
49
+ def process_wechat_links(html_content):
50
+ if not html_content:
51
+ return False
52
+
53
+ soup = BeautifulSoup(html_content, "html.parser")
54
+
55
+ # 提取链接
56
+ links = [a.get("href") for a in soup.find_all("a", href=True)]
57
+ target_links = [
58
+ link
59
+ for link in links
60
+ if "download" in link or "ftn" in link or "qq.com" in link # type: ignore
61
+ ]
62
+
63
+ if not target_links:
64
+ logging.warning("未找到符合条件的下载链接")
65
+ return False
66
+
67
+ download_url = target_links[0]
68
+ logging.info(f"提取到下载链接: {download_url}")
69
+
70
+ try:
71
+ headers = {
72
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
73
+ }
74
+ r = requests.get(download_url, headers=headers, stream=True) # type: ignore
75
+
76
+ if r.status_code == 200:
77
+ # import time
78
+
79
+ # timestamp = int(time.time())
80
+ today_str = datetime.date.today().strftime("%Y%m%d")
81
+ filename = f"{today_str}.zip"
82
+ if not os.path.exists(DOWNLOAD_FOLDER_WECHAT):
83
+ os.makedirs(DOWNLOAD_FOLDER_WECHAT)
84
+
85
+ filepath = os.path.join(DOWNLOAD_FOLDER_WECHAT, filename)
86
+
87
+ logging.info(f"准备保存文件到: {filepath}")
88
+
89
+ with open(filepath, "wb") as f:
90
+ for chunk in r.iter_content(chunk_size=8192):
91
+ f.write(chunk)
92
+
93
+ logging.info(f"链接下载成功: {filename}")
94
+ return True
95
+ else:
96
+ logging.error(f"下载链接访问失败: {r.status_code}")
97
+ return False
98
+
99
+ except Exception as e:
100
+ logging.error(f"下载异常: {e}")
101
+ return False
102
+
103
+
104
+ def download_safe_mode() -> bool:
105
+ try:
106
+ logging.info("正在连接 Gmail...")
107
+ mail = imaplib.IMAP4_SSL(IMAP_SERVER)
108
+ mail.login(username, password)
109
+ mail.select("inbox")
110
+
111
+ # 搜索最近 2 天 (防止时区问题导致搜不到今天)
112
+ target_date = datetime.date.today() - datetime.timedelta(days=2)
113
+ today_str = target_date.strftime("%Y/%m/%d")
114
+
115
+ query_safe = f'X-GM-RAW "after:{today_str}"'
116
+ logging.info(f"步骤1: 向服务器请求邮件 (query: {query_safe})")
117
+ status, messages = mail.search(None, query_safe)
118
+
119
+ if status != "OK":
120
+ logging.error("搜索失败。")
121
+ return False
122
+
123
+ email_ids = messages[0].split()
124
+ total_found = len(email_ids)
125
+ logging.info(f"步骤1完成: 找到 {total_found} 封邮件。")
126
+
127
+ if total_found == 0:
128
+ return False
129
+
130
+ logging.info(f"步骤2: 正在本地筛选包含关键词 '{SEARCH_KEYWORD}' 的邮件...\n")
131
+
132
+ match_count = 0
133
+ match_email_id = []
134
+ for email_id in reversed(email_ids):
135
+ res, msg_data = mail.fetch(email_id, "(RFC822)")
136
+ msg = email.message_from_bytes(msg_data[0][1]) # type: ignore
137
+
138
+ subject = clean_text(msg["Subject"])
139
+
140
+ match = re.search(SEARCH_KEYWORD, subject)
141
+ if not match:
142
+ continue
143
+
144
+ key = match.group()
145
+ logging.info(f"当前关键词为:{key}")
146
+
147
+ match_count += 1
148
+ match_email_id.append(email_id)
149
+ logging.info(f" [√ 匹配] 发现目标邮件: {subject}")
150
+
151
+ # === 分支逻辑 ===
152
+ if key == "微信":
153
+ logging.info("检测到微信账单,尝试提取下载链接...")
154
+ html_content = get_html_content(msg)
155
+ link_success = False
156
+ if html_content:
157
+ link_success = process_wechat_links(html_content)
158
+ if not link_success:
159
+ logging.info("链接提取失败或未找到,尝试检查是否有普通附件...")
160
+
161
+ else:
162
+ logging.info("检测到支付宝或其他,执行附件下载...")
163
+ for part in msg.walk():
164
+ if (
165
+ part.get_content_maintype() == "multipart"
166
+ or part.get("Content-Disposition") is None
167
+ ):
168
+ continue
169
+
170
+ filename = part.get_filename()
171
+ if filename:
172
+ filename = clean_text(filename)
173
+ if not os.path.exists(DOWNLOAD_FOLDER_ALI):
174
+ os.makedirs(DOWNLOAD_FOLDER_ALI)
175
+
176
+ today_str = datetime.date.today().strftime("%Y%m%d")
177
+ filepath = os.path.join(DOWNLOAD_FOLDER_ALI, f"{today_str}.zip")
178
+
179
+ with open(filepath, "wb") as f:
180
+ f.write(part.get_payload(decode=True)) # type: ignore
181
+
182
+ logging.info(f" -> 附件已保存: {today_str}")
183
+
184
+ # ... (后续的归档/移动邮件逻辑保持不变) ...
185
+
186
+ mail.close()
187
+ mail.logout()
188
+ return True
189
+
190
+ except Exception as e:
191
+ logging.error(f"发生错误: {e}")
192
+ return False
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "download-flow-enmu"
7
+ version = "0.1.0"
8
+ description = "A task flow management tool for syncing todo files with taskwarrior"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ {name = "Your Name", email = "your.email@example.com"}
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ ]
22
+
23
+ dependencies = [
24
+ "typer>=0.9.0",
25
+ "pydantic>=2.0.0",
26
+ "imap_tools",
27
+ "python-dotenv>=1.0.0"
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=7.0.0",
33
+ ]
34
+
35
+ [project.scripts]
36
+ dflow = "cli.main:main"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["."]
40
+ include = ["package*","cli*","config*","data*","factory*","strategy*"]
41
+ exclude = ["test*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ import datetime
2
+ import logging
3
+
4
+ from imap_tools import MailBox, AND
5
+
6
+ from strategy.downloader.provider import Downloader
7
+ from typing import override
8
+ from data.bill_profile import BillProfile
9
+
10
+
11
+ class AttchmentDownloader(Downloader):
12
+ @override
13
+ def download(self, mail: MailBox, bill: BillProfile):
14
+
15
+ criteria = AND(
16
+ from_=bill.sender_email,
17
+ date=datetime.date.today(),
18
+ )
19
+
20
+ for msg in mail.fetch(criteria=criteria):
21
+ logging.info(f"找到邮件: {msg.subject}") # 添加这一行
22
+
23
+ if bill.search_subject not in msg.subject:
24
+ continue
25
+ for att in msg.attachments:
26
+ if att.filename.lower().endswith(bill.file_suffix):
27
+ bill.save_subdir.mkdir(parents=True, exist_ok=True)
28
+ filepath = (
29
+ bill.save_subdir / f"{datetime.date.today()}.{bill.file_suffix}"
30
+ ) # 改这里:加扩展名
31
+
32
+ try:
33
+ if bill.file_suffix in ["zip", "7z", "rar"]:
34
+ filepath.write_bytes(att.payload)
35
+ else:
36
+ content_str = att.payload.decode(bill.encoding)
37
+ filepath.write_text(content_str, encoding="utf-8")
38
+ except UnicodeDecodeError:
39
+ logging.error(" ✗ 转码失败")
40
+ else:
41
+ logging.info(f"✗ 附件类型不匹配 (期望后缀: {bill.file_suffix})")
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+ from imap_tools import MailBox
3
+ from data.bill_profile import BillProfile
4
+
5
+
6
+ class Downloader(ABC):
7
+ @abstractmethod
8
+ def download(self, mail: MailBox, bill: BillProfile):
9
+ pass
File without changes
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+ from imap_tools import MailBox
3
+
4
+
5
+ class Email(ABC):
6
+
7
+ @abstractmethod
8
+ def connect() -> MailBox:
9
+ pass
@@ -0,0 +1,19 @@
1
+ import logging
2
+
3
+ from datetime import date
4
+ from strategy.email.email_provider import Email
5
+ from imap_tools import MailBox
6
+ from strategy.downloader.attchment import AttchmentDownloader
7
+
8
+
9
+ class Gmail(Email):
10
+ username: str
11
+ password: str
12
+
13
+ def __init__(self, username, password):
14
+ self.username = username
15
+ self.password = password
16
+ self.host = "imap.gmail.com"
17
+
18
+ def connect(self) -> MailBox:
19
+ return MailBox(self.host)
@@ -0,0 +1,31 @@
1
+ import logging
2
+
3
+ from pathlib import Path
4
+
5
+ from strategy.provider.provider import Provider
6
+ from strategy.email.email_provider import Email
7
+ from strategy.downloader.provider import Downloader
8
+ from data.bill_profile import BillProfile
9
+
10
+
11
+ class AliProvider(Provider):
12
+
13
+ downloader: Downloader
14
+ email: Email
15
+
16
+ def __init__(
17
+ self,
18
+ email: Email = None,
19
+ downloader: Downloader = None,
20
+ bill: BillProfile = None,
21
+ ):
22
+ self.email = email
23
+ self.downloader = downloader
24
+ self.bill = bill
25
+
26
+ def process_bills(self):
27
+ logging.info("开始处理ali账单")
28
+ mailbox = self.email.connect()
29
+
30
+ with mailbox.login(self.email.username, self.email.password, "INBOX") as mb:
31
+ self.downloader.download(mb, bill=self.bill)
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class Provider(ABC):
5
+
6
+ @abstractmethod
7
+ def process_bills(self):
8
+ pass