ErisPulse-EmailAdapter 1.0.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.
- erispulse_emailadapter-1.0.0/ErisPulse_EmailAdapter/Core.py +503 -0
- erispulse_emailadapter-1.0.0/ErisPulse_EmailAdapter/__init__.py +1 -0
- erispulse_emailadapter-1.0.0/ErisPulse_EmailAdapter.egg-info/PKG-INFO +115 -0
- erispulse_emailadapter-1.0.0/ErisPulse_EmailAdapter.egg-info/SOURCES.txt +11 -0
- erispulse_emailadapter-1.0.0/ErisPulse_EmailAdapter.egg-info/dependency_links.txt +1 -0
- erispulse_emailadapter-1.0.0/ErisPulse_EmailAdapter.egg-info/entry_points.txt +3 -0
- erispulse_emailadapter-1.0.0/ErisPulse_EmailAdapter.egg-info/top_level.txt +1 -0
- erispulse_emailadapter-1.0.0/LICENSE +31 -0
- erispulse_emailadapter-1.0.0/PKG-INFO +115 -0
- erispulse_emailadapter-1.0.0/README.md +105 -0
- erispulse_emailadapter-1.0.0/pyproject.toml +19 -0
- erispulse_emailadapter-1.0.0/setup.cfg +4 -0
- erispulse_emailadapter-1.0.0/tests/test.py +60 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uuid
|
|
3
|
+
import email
|
|
4
|
+
import smtplib
|
|
5
|
+
import imaplib
|
|
6
|
+
from email.mime.multipart import MIMEMultipart
|
|
7
|
+
from email.mime.text import MIMEText
|
|
8
|
+
from email.mime.application import MIMEApplication
|
|
9
|
+
from typing import Dict, List, Optional, Union, BinaryIO
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import ssl
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from ErisPulse import sdk
|
|
16
|
+
from ErisPulse.Core import BaseAdapter
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class EmailAccountConfig:
|
|
20
|
+
email: str
|
|
21
|
+
password: str
|
|
22
|
+
imap_server: Optional[str] = None
|
|
23
|
+
imap_port: Optional[int] = None
|
|
24
|
+
smtp_server: Optional[str] = None
|
|
25
|
+
smtp_port: Optional[int] = None
|
|
26
|
+
ssl: bool = True
|
|
27
|
+
timeout: int = 30
|
|
28
|
+
|
|
29
|
+
class EmailAdapter(BaseAdapter):
|
|
30
|
+
|
|
31
|
+
def __init__(self, sdk):
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.sdk = sdk or sdk
|
|
34
|
+
self.logger = self.sdk.logger
|
|
35
|
+
self.env = self.sdk.env
|
|
36
|
+
|
|
37
|
+
# 加载配置
|
|
38
|
+
self.global_config = self._load_global_config()
|
|
39
|
+
self.accounts: Dict[str, EmailAccountConfig] = self._load_account_configs()
|
|
40
|
+
|
|
41
|
+
# 连接池
|
|
42
|
+
self.smtp_connections: Dict[str, smtplib.SMTP] = {}
|
|
43
|
+
self.imap_connections: Dict[str, imaplib.IMAP4_SSL] = {}
|
|
44
|
+
|
|
45
|
+
# 轮询任务
|
|
46
|
+
self.poll_tasks: Dict[str, asyncio.Task] = {}
|
|
47
|
+
|
|
48
|
+
# 初始化状态
|
|
49
|
+
self._is_running = False
|
|
50
|
+
|
|
51
|
+
def _load_global_config(self) -> Dict:
|
|
52
|
+
config = self.env.getConfig("EmailAdapter.global", {})
|
|
53
|
+
|
|
54
|
+
if not config:
|
|
55
|
+
# 设置默认全局配置
|
|
56
|
+
defaults = {
|
|
57
|
+
"imap_server": "imap.example.com",
|
|
58
|
+
"imap_port": 993,
|
|
59
|
+
"smtp_server": "smtp.example.com",
|
|
60
|
+
"smtp_port": 465,
|
|
61
|
+
"ssl": True,
|
|
62
|
+
"timeout": 30,
|
|
63
|
+
"poll_interval": 60,
|
|
64
|
+
"max_retries": 3
|
|
65
|
+
}
|
|
66
|
+
self.env.setConfig("EmailAdapter.global", defaults)
|
|
67
|
+
return defaults
|
|
68
|
+
return config
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _load_account_configs(self) -> Dict[str, EmailAccountConfig]:
|
|
72
|
+
accounts = {}
|
|
73
|
+
account_configs = self.env.getConfig("EmailAdapter.accounts", {})
|
|
74
|
+
|
|
75
|
+
if not account_configs:
|
|
76
|
+
self.logger.warning("未找到任何账号配置,创建默认账号配置")
|
|
77
|
+
|
|
78
|
+
# 设置默认账号配置
|
|
79
|
+
defaults = {
|
|
80
|
+
"default": {
|
|
81
|
+
"email": "user@example.com",
|
|
82
|
+
"password": "password",
|
|
83
|
+
"server": {
|
|
84
|
+
"imap_server": self.global_config["imap_server"],
|
|
85
|
+
"imap_port": self.global_config["imap_port"],
|
|
86
|
+
"smtp_server": self.global_config["smtp_server"],
|
|
87
|
+
"smtp_port": self.global_config["smtp_port"],
|
|
88
|
+
"ssl": self.global_config["ssl"],
|
|
89
|
+
"timeout": self.global_config["timeout"]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
self.env.setConfig("EmailAdapter.accounts", defaults)
|
|
94
|
+
account_configs = defaults
|
|
95
|
+
|
|
96
|
+
for account_name, config in account_configs.items():
|
|
97
|
+
# 合并全局配置和账号特定配置
|
|
98
|
+
merged_config = {
|
|
99
|
+
"email": config.get("email"),
|
|
100
|
+
"password": config.get("password"),
|
|
101
|
+
"imap_server": config.get("server", {}).get("imap_server", self.global_config["imap_server"]),
|
|
102
|
+
"imap_port": config.get("server", {}).get("imap_port", self.global_config["imap_port"]),
|
|
103
|
+
"smtp_server": config.get("server", {}).get("smtp_server", self.global_config["smtp_server"]),
|
|
104
|
+
"smtp_port": config.get("server", {}).get("smtp_port", self.global_config["smtp_port"]),
|
|
105
|
+
"ssl": config.get("server", {}).get("ssl", self.global_config["ssl"]),
|
|
106
|
+
"timeout": config.get("server", {}).get("timeout", self.global_config["timeout"]),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
accounts[account_name] = EmailAccountConfig(**merged_config)
|
|
110
|
+
|
|
111
|
+
return accounts
|
|
112
|
+
|
|
113
|
+
class Send(BaseAdapter.Send):
|
|
114
|
+
"""邮件发送DSL"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, adapter, target_type=None, target_id=None, _account_id=None):
|
|
117
|
+
super().__init__(adapter, target_type, target_id, _account_id)
|
|
118
|
+
self._subject = ""
|
|
119
|
+
self._html = ""
|
|
120
|
+
self._text = ""
|
|
121
|
+
self._attachments = []
|
|
122
|
+
self._cc = []
|
|
123
|
+
self._bcc = []
|
|
124
|
+
self._reply_to = None
|
|
125
|
+
|
|
126
|
+
def Subject(self, subject: str):
|
|
127
|
+
"""设置邮件主题"""
|
|
128
|
+
self._subject = subject
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def Html(self, html: str):
|
|
132
|
+
"""设置HTML内容并发送邮件"""
|
|
133
|
+
self._html = html
|
|
134
|
+
return self._send()
|
|
135
|
+
|
|
136
|
+
def Text(self, text: str):
|
|
137
|
+
"""设置纯文本内容并发送邮件"""
|
|
138
|
+
self._text = text
|
|
139
|
+
return self._send()
|
|
140
|
+
|
|
141
|
+
def Attachment(self, file: Union[str, Path, BinaryIO], filename: str = None,
|
|
142
|
+
mime_type: str = "application/octet-stream"):
|
|
143
|
+
"""添加附件"""
|
|
144
|
+
self._attachments.append((file, filename, mime_type))
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def Cc(self, emails: Union[str, List[str]]):
|
|
148
|
+
"""设置抄送"""
|
|
149
|
+
if isinstance(emails, str):
|
|
150
|
+
emails = [emails]
|
|
151
|
+
self._cc.extend(emails)
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def Bcc(self, emails: Union[str, List[str]]):
|
|
155
|
+
"""设置密送"""
|
|
156
|
+
if isinstance(emails, str):
|
|
157
|
+
emails = [emails]
|
|
158
|
+
self._bcc.extend(emails)
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
def ReplyTo(self, email: str):
|
|
162
|
+
"""设置回复地址"""
|
|
163
|
+
self._reply_to = email
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
async def _send(self):
|
|
167
|
+
"""内部发送方法"""
|
|
168
|
+
if not self._account_id and not self._adapter.accounts:
|
|
169
|
+
raise ValueError("No email account configured")
|
|
170
|
+
|
|
171
|
+
account_id = self._account_id or next(iter(self._adapter.accounts.keys()))
|
|
172
|
+
|
|
173
|
+
if account_id not in self._adapter.accounts:
|
|
174
|
+
raise ValueError(f"Account {account_id} not found")
|
|
175
|
+
|
|
176
|
+
account = self._adapter.accounts[account_id]
|
|
177
|
+
|
|
178
|
+
# 构建邮件
|
|
179
|
+
msg = MIMEMultipart()
|
|
180
|
+
msg["From"] = account.email
|
|
181
|
+
msg["To"] = self._target_id if self._target_id else account.email
|
|
182
|
+
msg["Subject"] = self._subject
|
|
183
|
+
|
|
184
|
+
if self._cc:
|
|
185
|
+
msg["Cc"] = ", ".join(self._cc)
|
|
186
|
+
if self._bcc:
|
|
187
|
+
msg["Bcc"] = ", ".join(self._bcc)
|
|
188
|
+
if self._reply_to:
|
|
189
|
+
msg["Reply-To"] = self._reply_to
|
|
190
|
+
|
|
191
|
+
# 添加正文
|
|
192
|
+
if self._text:
|
|
193
|
+
msg.attach(MIMEText(self._text, "plain"))
|
|
194
|
+
if self._html:
|
|
195
|
+
msg.attach(MIMEText(self._html, "html"))
|
|
196
|
+
|
|
197
|
+
# 添加附件
|
|
198
|
+
for attachment in self._attachments:
|
|
199
|
+
file, filename, mime_type = attachment
|
|
200
|
+
|
|
201
|
+
if isinstance(file, (str, Path)):
|
|
202
|
+
with open(file, "rb") as f:
|
|
203
|
+
part = MIMEApplication(f.read(), Name=filename or Path(file).name)
|
|
204
|
+
else:
|
|
205
|
+
part = MIMEApplication(file.read(), Name=filename)
|
|
206
|
+
|
|
207
|
+
part["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
208
|
+
msg.attach(part)
|
|
209
|
+
|
|
210
|
+
# 发送邮件
|
|
211
|
+
try:
|
|
212
|
+
await self._adapter._send_email(account_id, msg)
|
|
213
|
+
return {
|
|
214
|
+
"status": "ok",
|
|
215
|
+
"retcode": 0,
|
|
216
|
+
"data": {"message": "Email sent successfully"},
|
|
217
|
+
"message": "Email sent successfully",
|
|
218
|
+
"message_id": uuid.uuid4().hex,
|
|
219
|
+
}
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self._adapter.logger.error(f"Failed to send email: {str(e)}")
|
|
222
|
+
return {
|
|
223
|
+
"status": "failed",
|
|
224
|
+
"retcode": 34000,
|
|
225
|
+
"data": None,
|
|
226
|
+
"message": str(e),
|
|
227
|
+
"message_id": "",
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async def _send_email(self, account_id: str, msg: MIMEMultipart):
|
|
231
|
+
"""实际发送邮件"""
|
|
232
|
+
account = self.accounts[account_id]
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if account_id not in self.smtp_connections:
|
|
236
|
+
await self._connect_smtp(account_id)
|
|
237
|
+
|
|
238
|
+
smtp = self.smtp_connections[account_id]
|
|
239
|
+
smtp.send_message(msg)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self.logger.error(f"SMTP error: {str(e)}")
|
|
242
|
+
# 尝试重新连接
|
|
243
|
+
await self._connect_smtp(account_id)
|
|
244
|
+
smtp = self.smtp_connections[account_id]
|
|
245
|
+
smtp.send_message(msg)
|
|
246
|
+
|
|
247
|
+
async def _connect_smtp(self, account_id: str):
|
|
248
|
+
"""连接SMTP服务器"""
|
|
249
|
+
account = self.accounts[account_id]
|
|
250
|
+
|
|
251
|
+
if account_id in self.smtp_connections:
|
|
252
|
+
try:
|
|
253
|
+
self.smtp_connections[account_id].quit()
|
|
254
|
+
except:
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
context = ssl.create_default_context()
|
|
258
|
+
|
|
259
|
+
if account.ssl:
|
|
260
|
+
smtp = smtplib.SMTP_SSL(
|
|
261
|
+
host=account.smtp_server,
|
|
262
|
+
port=account.smtp_port,
|
|
263
|
+
timeout=account.timeout,
|
|
264
|
+
context=context
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
smtp = smtplib.SMTP(
|
|
268
|
+
host=account.smtp_server,
|
|
269
|
+
port=account.smtp_port,
|
|
270
|
+
timeout=account.timeout
|
|
271
|
+
)
|
|
272
|
+
if account.ssl:
|
|
273
|
+
smtp.starttls(context=context)
|
|
274
|
+
|
|
275
|
+
smtp.login(account.email, account.password)
|
|
276
|
+
self.smtp_connections[account_id] = smtp
|
|
277
|
+
|
|
278
|
+
async def _connect_imap(self, account_id: str):
|
|
279
|
+
"""连接IMAP服务器"""
|
|
280
|
+
account = self.accounts[account_id]
|
|
281
|
+
|
|
282
|
+
if account_id in self.imap_connections:
|
|
283
|
+
try:
|
|
284
|
+
self.imap_connections[account_id].logout()
|
|
285
|
+
except:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
context = ssl.create_default_context()
|
|
289
|
+
|
|
290
|
+
imap = imaplib.IMAP4_SSL(
|
|
291
|
+
host=account.imap_server,
|
|
292
|
+
port=account.imap_port,
|
|
293
|
+
ssl_context=context
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
imap.login(account.email, account.password)
|
|
297
|
+
imap.select("INBOX")
|
|
298
|
+
self.imap_connections[account_id] = imap
|
|
299
|
+
|
|
300
|
+
async def _poll_emails(self, account_id: str):
|
|
301
|
+
"""轮询邮箱获取新邮件"""
|
|
302
|
+
poll_interval = self.global_config.get("poll_interval", 60)
|
|
303
|
+
max_retries = self.global_config.get("max_retries", 3)
|
|
304
|
+
|
|
305
|
+
while self._is_running:
|
|
306
|
+
try:
|
|
307
|
+
if account_id not in self.imap_connections:
|
|
308
|
+
await self._connect_imap(account_id)
|
|
309
|
+
|
|
310
|
+
imap = self.imap_connections[account_id]
|
|
311
|
+
imap.noop() # 保持连接活跃
|
|
312
|
+
|
|
313
|
+
# 搜索未读邮件
|
|
314
|
+
status, messages = imap.search(None, "UNSEEN")
|
|
315
|
+
if status == "OK" and messages[0]:
|
|
316
|
+
for num in messages[0].split():
|
|
317
|
+
status, data = imap.fetch(num, "(RFC822)")
|
|
318
|
+
if status == "OK":
|
|
319
|
+
raw_email = data[0][1]
|
|
320
|
+
email_message = email.message_from_bytes(raw_email)
|
|
321
|
+
|
|
322
|
+
# 转换为标准事件
|
|
323
|
+
event = self._convert_email_to_event(email_message, account_id)
|
|
324
|
+
await self.emit("email", event)
|
|
325
|
+
await sdk.adapter.emit(event)
|
|
326
|
+
|
|
327
|
+
await asyncio.sleep(poll_interval)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
self.logger.error(f"Polling error for {account_id}: {str(e)}")
|
|
330
|
+
retries = 0
|
|
331
|
+
while retries < max_retries:
|
|
332
|
+
try:
|
|
333
|
+
await self._connect_imap(account_id)
|
|
334
|
+
break
|
|
335
|
+
except Exception as e:
|
|
336
|
+
retries += 1
|
|
337
|
+
if retries >= max_retries:
|
|
338
|
+
self.logger.error(f"Failed to reconnect after {max_retries} attempts")
|
|
339
|
+
raise
|
|
340
|
+
await asyncio.sleep(5)
|
|
341
|
+
|
|
342
|
+
def _convert_email_to_event(self, email_message: email.message.Message, account_id: str) -> Dict:
|
|
343
|
+
"""将邮件转换为标准事件格式"""
|
|
344
|
+
# 解析邮件内容
|
|
345
|
+
def decode_header(header):
|
|
346
|
+
from email.header import decode_header
|
|
347
|
+
decoded = decode_header(header)
|
|
348
|
+
parts = []
|
|
349
|
+
for part, encoding in decoded:
|
|
350
|
+
if isinstance(part, bytes):
|
|
351
|
+
try:
|
|
352
|
+
part = part.decode(encoding or 'utf-8')
|
|
353
|
+
except:
|
|
354
|
+
part = part.decode('utf-8', errors='replace')
|
|
355
|
+
parts.append(part)
|
|
356
|
+
return ''.join(parts)
|
|
357
|
+
|
|
358
|
+
subject = decode_header(email_message.get("Subject", ""))
|
|
359
|
+
from_ = decode_header(email_message.get("From", ""))
|
|
360
|
+
to = decode_header(email_message.get("To", ""))
|
|
361
|
+
date = email_message.get("Date", "")
|
|
362
|
+
|
|
363
|
+
# 解析正文
|
|
364
|
+
text_content = ""
|
|
365
|
+
html_content = ""
|
|
366
|
+
attachments = []
|
|
367
|
+
|
|
368
|
+
for part in email_message.walk():
|
|
369
|
+
content_type = part.get_content_type()
|
|
370
|
+
content_disposition = str(part.get("Content-Disposition"))
|
|
371
|
+
|
|
372
|
+
if "attachment" in content_disposition:
|
|
373
|
+
# 处理附件
|
|
374
|
+
filename = part.get_filename()
|
|
375
|
+
if filename:
|
|
376
|
+
attachments.append({
|
|
377
|
+
"filename": filename,
|
|
378
|
+
"content_type": content_type,
|
|
379
|
+
"size": len(part.get_payload(decode=True)),
|
|
380
|
+
"data": part.get_payload(decode=True)
|
|
381
|
+
})
|
|
382
|
+
elif content_type == "text/plain":
|
|
383
|
+
# 纯文本内容
|
|
384
|
+
payload = part.get_payload(decode=True)
|
|
385
|
+
try:
|
|
386
|
+
text_content = payload.decode('utf-8')
|
|
387
|
+
except UnicodeDecodeError:
|
|
388
|
+
try:
|
|
389
|
+
text_content = payload.decode('gbk')
|
|
390
|
+
except UnicodeDecodeError:
|
|
391
|
+
text_content = payload.decode('utf-8', errors='replace')
|
|
392
|
+
elif content_type == "text/html":
|
|
393
|
+
# HTML内容
|
|
394
|
+
payload = part.get_payload(decode=True)
|
|
395
|
+
try:
|
|
396
|
+
html_content = payload.decode('utf-8')
|
|
397
|
+
except UnicodeDecodeError:
|
|
398
|
+
try:
|
|
399
|
+
html_content = payload.decode('gbk')
|
|
400
|
+
except UnicodeDecodeError:
|
|
401
|
+
html_content = payload.decode('utf-8', errors='replace')
|
|
402
|
+
|
|
403
|
+
# 构建标准事件
|
|
404
|
+
return {
|
|
405
|
+
"id": email_message.get("Message-ID", ""),
|
|
406
|
+
"time": self._parse_email_date(date),
|
|
407
|
+
"type": "message",
|
|
408
|
+
"detail_type": "private", # 邮件默认为私聊
|
|
409
|
+
"platform": "email",
|
|
410
|
+
"self": {
|
|
411
|
+
"platform": "email",
|
|
412
|
+
"user_id": account_id
|
|
413
|
+
},
|
|
414
|
+
"message": [
|
|
415
|
+
{
|
|
416
|
+
"type": "text",
|
|
417
|
+
"data": {
|
|
418
|
+
"text": f"Subject: {subject}\nFrom: {from_}\n\n{text_content}"
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
"alt_message": f"邮件: {subject}",
|
|
423
|
+
"user_id": from_,
|
|
424
|
+
"email_raw": {
|
|
425
|
+
"subject": subject,
|
|
426
|
+
"from": from_,
|
|
427
|
+
"to": to,
|
|
428
|
+
"date": date,
|
|
429
|
+
"text_content": text_content,
|
|
430
|
+
"html_content": html_content,
|
|
431
|
+
"attachments": [att["filename"] for att in attachments]
|
|
432
|
+
},
|
|
433
|
+
"attachments": attachments
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
def _parse_email_date(self, date_str: str) -> int:
|
|
437
|
+
"""解析邮件日期为时间戳"""
|
|
438
|
+
from email.utils import parsedate_to_datetime
|
|
439
|
+
try:
|
|
440
|
+
dt = parsedate_to_datetime(date_str)
|
|
441
|
+
return int(dt.timestamp())
|
|
442
|
+
except:
|
|
443
|
+
return int(time.time())
|
|
444
|
+
|
|
445
|
+
async def call_api(self, endpoint: str, **params):
|
|
446
|
+
"""实现BaseAdapter要求的call_api方法"""
|
|
447
|
+
if endpoint == "send":
|
|
448
|
+
return await self.Send(self).Text(params.get("content", "")).send()
|
|
449
|
+
elif endpoint == "send_html":
|
|
450
|
+
return await self.Send(self).Html(params.get("html", "")).send()
|
|
451
|
+
else:
|
|
452
|
+
return {
|
|
453
|
+
"status": "failed",
|
|
454
|
+
"retcode": 10002,
|
|
455
|
+
"data": None,
|
|
456
|
+
"message": f"Unsupported endpoint: {endpoint}",
|
|
457
|
+
"message_id": ""
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async def start(self):
|
|
461
|
+
"""启动适配器"""
|
|
462
|
+
if not self.accounts:
|
|
463
|
+
self.logger.warning("No email accounts configured")
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
self._is_running = True
|
|
467
|
+
|
|
468
|
+
# 连接所有SMTP服务器
|
|
469
|
+
for account_id in self.accounts:
|
|
470
|
+
try:
|
|
471
|
+
await self._connect_smtp(account_id)
|
|
472
|
+
except Exception as e:
|
|
473
|
+
self.logger.error(f"Failed to connect SMTP for {account_id}: {str(e)}")
|
|
474
|
+
|
|
475
|
+
# 启动轮询任务
|
|
476
|
+
for account_id in self.accounts:
|
|
477
|
+
if self.accounts[account_id].imap_server: # 只有配置了IMAP的账号才轮询
|
|
478
|
+
self.poll_tasks[account_id] = asyncio.create_task(self._poll_emails(account_id))
|
|
479
|
+
|
|
480
|
+
async def shutdown(self):
|
|
481
|
+
"""关闭适配器"""
|
|
482
|
+
self._is_running = False
|
|
483
|
+
|
|
484
|
+
# 取消所有轮询任务
|
|
485
|
+
for task in self.poll_tasks.values():
|
|
486
|
+
task.cancel()
|
|
487
|
+
self.poll_tasks.clear()
|
|
488
|
+
|
|
489
|
+
# 关闭所有SMTP连接
|
|
490
|
+
for account_id, smtp in self.smtp_connections.items():
|
|
491
|
+
try:
|
|
492
|
+
smtp.quit()
|
|
493
|
+
except:
|
|
494
|
+
pass
|
|
495
|
+
self.smtp_connections.clear()
|
|
496
|
+
|
|
497
|
+
# 关闭所有IMAP连接
|
|
498
|
+
for account_id, imap in self.imap_connections.items():
|
|
499
|
+
try:
|
|
500
|
+
imap.logout()
|
|
501
|
+
except:
|
|
502
|
+
pass
|
|
503
|
+
self.imap_connections.clear()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .Core import EmailAdapter
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ErisPulse-EmailAdapter
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ErisPulse邮箱适配器,支持多账号管理、HTML内容和附件发送
|
|
5
|
+
Author-email: wsu2059q <wsu2059@qq.com>
|
|
6
|
+
Project-URL: homepage, https://github.com/ErisPulse/ErisPulse-EmailAdapter
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# ErisPulse 邮箱适配器
|
|
12
|
+
|
|
13
|
+
一个为 ErisPulse 框架设计的强大邮箱适配器,支持在 ErisPulse 生态系统中以事件形式收发电子邮件。
|
|
14
|
+
|
|
15
|
+
## 功能特性
|
|
16
|
+
|
|
17
|
+
- **多账户支持**:可配置多个邮箱账户,每个账户可设置独立的SMTP/IMAP参数
|
|
18
|
+
- **全局配置**:可定义全局SMTP和IMAP设置,所有账户均可继承
|
|
19
|
+
- **邮件轮询**:自动检查邮箱账户中的新邮件(未读邮件),并将其转换为标准ErisPulse事件
|
|
20
|
+
- **DSL发送**:使用标准ErisPulse `Send` DSL轻松发送邮件
|
|
21
|
+
- **附件支持**:支持发送和接收邮件附件
|
|
22
|
+
- **HTML与纯文本**:同时支持HTML和纯文本邮件内容
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
像安装其他ErisPulse模块或适配器一样安装本适配器:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
epsdk install Email
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
如果与ErisPulse安装在同一个Python环境中,它将被自动发现。
|
|
33
|
+
|
|
34
|
+
## 配置
|
|
35
|
+
|
|
36
|
+
首次运行后,适配器会在您的`config.toml`文件中生成默认配置块。您需要用真实的邮箱账户信息更新它。
|
|
37
|
+
|
|
38
|
+
### 配置示例
|
|
39
|
+
|
|
40
|
+
```toml
|
|
41
|
+
[EmailAdapter.global]
|
|
42
|
+
imap_server = "imap.example.com" # 全局IMAP服务器
|
|
43
|
+
imap_port = 993 # IMAP端口
|
|
44
|
+
smtp_server = "smtp.example.com" # 全局SMTP服务器
|
|
45
|
+
smtp_port = 465 # SMTP端口
|
|
46
|
+
ssl = true # 使用SSL/TLS加密
|
|
47
|
+
timeout = 30 # 连接超时时间(秒)
|
|
48
|
+
poll_interval = 10 # 邮件轮询间隔(秒)
|
|
49
|
+
max_retries = 3 # 失败连接的最大重试次数
|
|
50
|
+
|
|
51
|
+
[EmailAdapter.accounts."support@example.com"]
|
|
52
|
+
email = "support@example.com" # 账户邮箱地址
|
|
53
|
+
password = "yourpassword" # 账户密码
|
|
54
|
+
|
|
55
|
+
[EmailAdapter.accounts."user@example.com"]
|
|
56
|
+
email = "user@example.com"
|
|
57
|
+
password = "anotherpassword"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 使用方法
|
|
61
|
+
|
|
62
|
+
### 发送邮件
|
|
63
|
+
|
|
64
|
+
使用标准ErisPulse `Send` DSL发送邮件。接收者应为目标邮箱地址。
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from ErisPulse import sdk
|
|
68
|
+
|
|
69
|
+
# 从默认账户发送
|
|
70
|
+
await sdk.adapter.mail.Send.To("recipient@example.com").Text("来自ErisPulse的问候!")
|
|
71
|
+
|
|
72
|
+
# 从特定账户发送带主题的邮件
|
|
73
|
+
await sdk.adapter.mail.Send.Using("support@example.com").To("client@company.com") \
|
|
74
|
+
.Subject("重要更新"). \
|
|
75
|
+
.Attachment("document.pdf").
|
|
76
|
+
Text("请查看附件中的文档。") \
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# 发送HTML邮件
|
|
80
|
+
html_content = """
|
|
81
|
+
<h1>欢迎!</h1>
|
|
82
|
+
<p>感谢使用我们的服务。</p>
|
|
83
|
+
"""
|
|
84
|
+
await sdk.adapter.mail.Send.To("user@example.com") \
|
|
85
|
+
.Subject("HTML邮件").Html(html_content)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 接收邮件
|
|
89
|
+
|
|
90
|
+
收到的邮件会自动转换为标准`message`事件。您可以像监听其他消息一样监听它们。
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from ErisPulse import sdk, adapter
|
|
94
|
+
|
|
95
|
+
@adapter.on("message")
|
|
96
|
+
async def handle_email_messages(data: dict):
|
|
97
|
+
# 检查消息是否来自邮箱适配器
|
|
98
|
+
if data.get("platform") == "mail":
|
|
99
|
+
sender = data.get("user_id")
|
|
100
|
+
subject = data["email_raw"]["subject"]
|
|
101
|
+
content = data["email_raw"]["text_content"]
|
|
102
|
+
|
|
103
|
+
print(f"新邮件来自: {sender}")
|
|
104
|
+
print(f"主题: {subject}")
|
|
105
|
+
print(f"内容:\n{content}")
|
|
106
|
+
|
|
107
|
+
# 检查附件
|
|
108
|
+
if data.get("attachments"):
|
|
109
|
+
print(f"附件: {[a['filename'] for a in data['attachments']]}")
|
|
110
|
+
|
|
111
|
+
# 自动回复示例
|
|
112
|
+
await sdk.adapter.mail.Send.To(sender) \
|
|
113
|
+
.Subject(f"回复: {subject}") \
|
|
114
|
+
.Text("已收到您的邮件,我们将尽快回复。")
|
|
115
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
ErisPulse_EmailAdapter/Core.py
|
|
5
|
+
ErisPulse_EmailAdapter/__init__.py
|
|
6
|
+
ErisPulse_EmailAdapter.egg-info/PKG-INFO
|
|
7
|
+
ErisPulse_EmailAdapter.egg-info/SOURCES.txt
|
|
8
|
+
ErisPulse_EmailAdapter.egg-info/dependency_links.txt
|
|
9
|
+
ErisPulse_EmailAdapter.egg-info/entry_points.txt
|
|
10
|
+
ErisPulse_EmailAdapter.egg-info/top_level.txt
|
|
11
|
+
tests/test.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ErisPulse_EmailAdapter
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ErisPulse
|
|
4
|
+
|
|
5
|
+
Portions of this software are based on https://github.com/runoneall/sdkFrame.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
24
|
+
|
|
25
|
+
This SDK's core adapter standardization layer strictly follows OneBot12 specifications (https://12.onebot.dev/) for:
|
|
26
|
+
- Event data normalization
|
|
27
|
+
- API request/response formats
|
|
28
|
+
- Message segment definitions
|
|
29
|
+
- Metadata definitions
|
|
30
|
+
|
|
31
|
+
This ensures consistent behavior and interoperability across all adapter implementations.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ErisPulse-EmailAdapter
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ErisPulse邮箱适配器,支持多账号管理、HTML内容和附件发送
|
|
5
|
+
Author-email: wsu2059q <wsu2059@qq.com>
|
|
6
|
+
Project-URL: homepage, https://github.com/ErisPulse/ErisPulse-EmailAdapter
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# ErisPulse 邮箱适配器
|
|
12
|
+
|
|
13
|
+
一个为 ErisPulse 框架设计的强大邮箱适配器,支持在 ErisPulse 生态系统中以事件形式收发电子邮件。
|
|
14
|
+
|
|
15
|
+
## 功能特性
|
|
16
|
+
|
|
17
|
+
- **多账户支持**:可配置多个邮箱账户,每个账户可设置独立的SMTP/IMAP参数
|
|
18
|
+
- **全局配置**:可定义全局SMTP和IMAP设置,所有账户均可继承
|
|
19
|
+
- **邮件轮询**:自动检查邮箱账户中的新邮件(未读邮件),并将其转换为标准ErisPulse事件
|
|
20
|
+
- **DSL发送**:使用标准ErisPulse `Send` DSL轻松发送邮件
|
|
21
|
+
- **附件支持**:支持发送和接收邮件附件
|
|
22
|
+
- **HTML与纯文本**:同时支持HTML和纯文本邮件内容
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
像安装其他ErisPulse模块或适配器一样安装本适配器:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
epsdk install Email
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
如果与ErisPulse安装在同一个Python环境中,它将被自动发现。
|
|
33
|
+
|
|
34
|
+
## 配置
|
|
35
|
+
|
|
36
|
+
首次运行后,适配器会在您的`config.toml`文件中生成默认配置块。您需要用真实的邮箱账户信息更新它。
|
|
37
|
+
|
|
38
|
+
### 配置示例
|
|
39
|
+
|
|
40
|
+
```toml
|
|
41
|
+
[EmailAdapter.global]
|
|
42
|
+
imap_server = "imap.example.com" # 全局IMAP服务器
|
|
43
|
+
imap_port = 993 # IMAP端口
|
|
44
|
+
smtp_server = "smtp.example.com" # 全局SMTP服务器
|
|
45
|
+
smtp_port = 465 # SMTP端口
|
|
46
|
+
ssl = true # 使用SSL/TLS加密
|
|
47
|
+
timeout = 30 # 连接超时时间(秒)
|
|
48
|
+
poll_interval = 10 # 邮件轮询间隔(秒)
|
|
49
|
+
max_retries = 3 # 失败连接的最大重试次数
|
|
50
|
+
|
|
51
|
+
[EmailAdapter.accounts."support@example.com"]
|
|
52
|
+
email = "support@example.com" # 账户邮箱地址
|
|
53
|
+
password = "yourpassword" # 账户密码
|
|
54
|
+
|
|
55
|
+
[EmailAdapter.accounts."user@example.com"]
|
|
56
|
+
email = "user@example.com"
|
|
57
|
+
password = "anotherpassword"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 使用方法
|
|
61
|
+
|
|
62
|
+
### 发送邮件
|
|
63
|
+
|
|
64
|
+
使用标准ErisPulse `Send` DSL发送邮件。接收者应为目标邮箱地址。
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from ErisPulse import sdk
|
|
68
|
+
|
|
69
|
+
# 从默认账户发送
|
|
70
|
+
await sdk.adapter.mail.Send.To("recipient@example.com").Text("来自ErisPulse的问候!")
|
|
71
|
+
|
|
72
|
+
# 从特定账户发送带主题的邮件
|
|
73
|
+
await sdk.adapter.mail.Send.Using("support@example.com").To("client@company.com") \
|
|
74
|
+
.Subject("重要更新"). \
|
|
75
|
+
.Attachment("document.pdf").
|
|
76
|
+
Text("请查看附件中的文档。") \
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# 发送HTML邮件
|
|
80
|
+
html_content = """
|
|
81
|
+
<h1>欢迎!</h1>
|
|
82
|
+
<p>感谢使用我们的服务。</p>
|
|
83
|
+
"""
|
|
84
|
+
await sdk.adapter.mail.Send.To("user@example.com") \
|
|
85
|
+
.Subject("HTML邮件").Html(html_content)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 接收邮件
|
|
89
|
+
|
|
90
|
+
收到的邮件会自动转换为标准`message`事件。您可以像监听其他消息一样监听它们。
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from ErisPulse import sdk, adapter
|
|
94
|
+
|
|
95
|
+
@adapter.on("message")
|
|
96
|
+
async def handle_email_messages(data: dict):
|
|
97
|
+
# 检查消息是否来自邮箱适配器
|
|
98
|
+
if data.get("platform") == "mail":
|
|
99
|
+
sender = data.get("user_id")
|
|
100
|
+
subject = data["email_raw"]["subject"]
|
|
101
|
+
content = data["email_raw"]["text_content"]
|
|
102
|
+
|
|
103
|
+
print(f"新邮件来自: {sender}")
|
|
104
|
+
print(f"主题: {subject}")
|
|
105
|
+
print(f"内容:\n{content}")
|
|
106
|
+
|
|
107
|
+
# 检查附件
|
|
108
|
+
if data.get("attachments"):
|
|
109
|
+
print(f"附件: {[a['filename'] for a in data['attachments']]}")
|
|
110
|
+
|
|
111
|
+
# 自动回复示例
|
|
112
|
+
await sdk.adapter.mail.Send.To(sender) \
|
|
113
|
+
.Subject(f"回复: {subject}") \
|
|
114
|
+
.Text("已收到您的邮件,我们将尽快回复。")
|
|
115
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# ErisPulse 邮箱适配器
|
|
2
|
+
|
|
3
|
+
一个为 ErisPulse 框架设计的强大邮箱适配器,支持在 ErisPulse 生态系统中以事件形式收发电子邮件。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **多账户支持**:可配置多个邮箱账户,每个账户可设置独立的SMTP/IMAP参数
|
|
8
|
+
- **全局配置**:可定义全局SMTP和IMAP设置,所有账户均可继承
|
|
9
|
+
- **邮件轮询**:自动检查邮箱账户中的新邮件(未读邮件),并将其转换为标准ErisPulse事件
|
|
10
|
+
- **DSL发送**:使用标准ErisPulse `Send` DSL轻松发送邮件
|
|
11
|
+
- **附件支持**:支持发送和接收邮件附件
|
|
12
|
+
- **HTML与纯文本**:同时支持HTML和纯文本邮件内容
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
像安装其他ErisPulse模块或适配器一样安装本适配器:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
epsdk install Email
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
如果与ErisPulse安装在同一个Python环境中,它将被自动发现。
|
|
23
|
+
|
|
24
|
+
## 配置
|
|
25
|
+
|
|
26
|
+
首次运行后,适配器会在您的`config.toml`文件中生成默认配置块。您需要用真实的邮箱账户信息更新它。
|
|
27
|
+
|
|
28
|
+
### 配置示例
|
|
29
|
+
|
|
30
|
+
```toml
|
|
31
|
+
[EmailAdapter.global]
|
|
32
|
+
imap_server = "imap.example.com" # 全局IMAP服务器
|
|
33
|
+
imap_port = 993 # IMAP端口
|
|
34
|
+
smtp_server = "smtp.example.com" # 全局SMTP服务器
|
|
35
|
+
smtp_port = 465 # SMTP端口
|
|
36
|
+
ssl = true # 使用SSL/TLS加密
|
|
37
|
+
timeout = 30 # 连接超时时间(秒)
|
|
38
|
+
poll_interval = 10 # 邮件轮询间隔(秒)
|
|
39
|
+
max_retries = 3 # 失败连接的最大重试次数
|
|
40
|
+
|
|
41
|
+
[EmailAdapter.accounts."support@example.com"]
|
|
42
|
+
email = "support@example.com" # 账户邮箱地址
|
|
43
|
+
password = "yourpassword" # 账户密码
|
|
44
|
+
|
|
45
|
+
[EmailAdapter.accounts."user@example.com"]
|
|
46
|
+
email = "user@example.com"
|
|
47
|
+
password = "anotherpassword"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 使用方法
|
|
51
|
+
|
|
52
|
+
### 发送邮件
|
|
53
|
+
|
|
54
|
+
使用标准ErisPulse `Send` DSL发送邮件。接收者应为目标邮箱地址。
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from ErisPulse import sdk
|
|
58
|
+
|
|
59
|
+
# 从默认账户发送
|
|
60
|
+
await sdk.adapter.mail.Send.To("recipient@example.com").Text("来自ErisPulse的问候!")
|
|
61
|
+
|
|
62
|
+
# 从特定账户发送带主题的邮件
|
|
63
|
+
await sdk.adapter.mail.Send.Using("support@example.com").To("client@company.com") \
|
|
64
|
+
.Subject("重要更新"). \
|
|
65
|
+
.Attachment("document.pdf").
|
|
66
|
+
Text("请查看附件中的文档。") \
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# 发送HTML邮件
|
|
70
|
+
html_content = """
|
|
71
|
+
<h1>欢迎!</h1>
|
|
72
|
+
<p>感谢使用我们的服务。</p>
|
|
73
|
+
"""
|
|
74
|
+
await sdk.adapter.mail.Send.To("user@example.com") \
|
|
75
|
+
.Subject("HTML邮件").Html(html_content)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 接收邮件
|
|
79
|
+
|
|
80
|
+
收到的邮件会自动转换为标准`message`事件。您可以像监听其他消息一样监听它们。
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from ErisPulse import sdk, adapter
|
|
84
|
+
|
|
85
|
+
@adapter.on("message")
|
|
86
|
+
async def handle_email_messages(data: dict):
|
|
87
|
+
# 检查消息是否来自邮箱适配器
|
|
88
|
+
if data.get("platform") == "mail":
|
|
89
|
+
sender = data.get("user_id")
|
|
90
|
+
subject = data["email_raw"]["subject"]
|
|
91
|
+
content = data["email_raw"]["text_content"]
|
|
92
|
+
|
|
93
|
+
print(f"新邮件来自: {sender}")
|
|
94
|
+
print(f"主题: {subject}")
|
|
95
|
+
print(f"内容:\n{content}")
|
|
96
|
+
|
|
97
|
+
# 检查附件
|
|
98
|
+
if data.get("attachments"):
|
|
99
|
+
print(f"附件: {[a['filename'] for a in data['attachments']]}")
|
|
100
|
+
|
|
101
|
+
# 自动回复示例
|
|
102
|
+
await sdk.adapter.mail.Send.To(sender) \
|
|
103
|
+
.Subject(f"回复: {subject}") \
|
|
104
|
+
.Text("已收到您的邮件,我们将尽快回复。")
|
|
105
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ErisPulse-EmailAdapter"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "ErisPulse邮箱适配器,支持多账号管理、HTML内容和附件发送"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{name = "wsu2059q", email = "wsu2059@qq.com"}
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
homepage = "https://github.com/ErisPulse/ErisPulse-EmailAdapter"
|
|
16
|
+
|
|
17
|
+
[project.entry-points."erispulse.adapter"]
|
|
18
|
+
"email" = "EmailAdapter:EmailAdapter"
|
|
19
|
+
"mail" = "EmailAdapter:EmailAdapter"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from ErisPulse import sdk
|
|
3
|
+
|
|
4
|
+
async def main():
|
|
5
|
+
# 初始化 ErisPulse SDK
|
|
6
|
+
if not sdk.init():
|
|
7
|
+
print("ErisPulse 初始化失败")
|
|
8
|
+
return
|
|
9
|
+
|
|
10
|
+
await sdk.adapter.startup()
|
|
11
|
+
|
|
12
|
+
await asyncio.sleep(1)
|
|
13
|
+
|
|
14
|
+
# 发送邮件并检查结果
|
|
15
|
+
result = await sdk.adapter.email.Send.To("suyu@anran.xyz").Text("Hello World", subject="测试邮件")
|
|
16
|
+
if result["status"] == "ok":
|
|
17
|
+
print(f"\n邮件发送成功! 消息ID: {result['message_id']}\n")
|
|
18
|
+
else:
|
|
19
|
+
print(f"\n邮件发送失败: {result['message']}\n")
|
|
20
|
+
|
|
21
|
+
@sdk.adapter.on("message")
|
|
22
|
+
async def handle_message(data):
|
|
23
|
+
if data.get("platform") == "email":
|
|
24
|
+
print("\n" + "="*50)
|
|
25
|
+
print(f"收到新邮件 (ID: {data['id']})")
|
|
26
|
+
print(f"时间: {data['time']}")
|
|
27
|
+
print(f"发件人: {data.get('email_from', '未知')}")
|
|
28
|
+
print(f"收件人: {data.get('email_to', '未知')}")
|
|
29
|
+
print(f"主题: {data.get('email_subject', '无主题')}")
|
|
30
|
+
print("-"*20 + " 内容 " + "-"*20)
|
|
31
|
+
|
|
32
|
+
# 打印消息内容
|
|
33
|
+
for segment in data.get("message", []):
|
|
34
|
+
if segment["type"] == "text":
|
|
35
|
+
print(segment["data"]["text"])
|
|
36
|
+
|
|
37
|
+
print("="*50 + "\n")
|
|
38
|
+
|
|
39
|
+
# 打印启动信息
|
|
40
|
+
print("="*50)
|
|
41
|
+
print("ErisPulse 邮箱适配器已启动")
|
|
42
|
+
print("正在监听新邮件...")
|
|
43
|
+
print("按 Ctrl+C 退出")
|
|
44
|
+
print("="*50)
|
|
45
|
+
|
|
46
|
+
# 保持程序运行
|
|
47
|
+
try:
|
|
48
|
+
while True:
|
|
49
|
+
await asyncio.sleep(1)
|
|
50
|
+
except asyncio.CancelledError:
|
|
51
|
+
pass
|
|
52
|
+
except KeyboardInterrupt:
|
|
53
|
+
print("\n收到停止信号,正在关闭...")
|
|
54
|
+
finally:
|
|
55
|
+
# 关闭适配器
|
|
56
|
+
await sdk.adapter.shutdown()
|
|
57
|
+
print("邮箱适配器已关闭")
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
asyncio.run(main())
|