mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-11 18:10:01 +08:00
feat: add imap proxy server (#225)
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
|
## main branch
|
||||||
|
|
||||||
|
- 用户名限制最长30个字符
|
||||||
|
- 修复 `/external/api/send_mail` 未返回的 bug (#222)
|
||||||
|
- 添加 `IMAP proxy` 服务,支持 `IMAP` 查看邮件
|
||||||
|
|
||||||
## v0.4.0
|
## v0.4.0
|
||||||
|
|
||||||
### DB Changes/Breaking changes
|
### DB Changes/Breaking changes
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -1,5 +1,20 @@
|
|||||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
# 使用 cloudflare 免费服务,搭建临时邮箱
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE">
|
||||||
|
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors">
|
||||||
|
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="">
|
||||||
|
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/dQEwTWhA6Q">
|
||||||
|
<img alt="Join Discord Chat" src="https://img.shields.io/discord/1238705663623036939.svg?label=discord&logo=discord">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
||||||
|
|
||||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
||||||
@@ -31,6 +46,7 @@
|
|||||||
- [在线演示](#在线演示)
|
- [在线演示](#在线演示)
|
||||||
- [功能/TODO](#功能todo)
|
- [功能/TODO](#功能todo)
|
||||||
- [Reference](#reference)
|
- [Reference](#reference)
|
||||||
|
- [Join Community](#join-community)
|
||||||
|
|
||||||
## 功能/TODO
|
## 功能/TODO
|
||||||
|
|
||||||
@@ -44,7 +60,7 @@
|
|||||||
- [x] 支持发送邮件
|
- [x] 支持发送邮件
|
||||||
- [x] 支持 `DKIM`
|
- [x] 支持 `DKIM`
|
||||||
- [x] `admin` 后台创建无前缀邮箱
|
- [x] `admin` 后台创建无前缀邮箱
|
||||||
- [x] 添加 `SMTP proxy server`,支持 SMTP 发送邮件
|
- [x] 添加 `SMTP proxy server`,支持 `SMTP` 发送邮件, `IMAP` 查看邮件
|
||||||
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
@@ -53,3 +69,7 @@
|
|||||||
- 使用 Cloudflare Pages 部署前端
|
- 使用 Cloudflare Pages 部署前端
|
||||||
- 使用 Cloudflare Workers 部署后端
|
- 使用 Cloudflare Workers 部署后端
|
||||||
- email 转发使用 Cloudflare Email Routing
|
- email 转发使用 Cloudflare Email Routing
|
||||||
|
|
||||||
|
## Join Community
|
||||||
|
|
||||||
|
- [Discord](https://discord.gg/dQEwTWhA6Q)
|
||||||
|
|||||||
21
smtp_proxy_server/config.py
Normal file
21
smtp_proxy_server/config.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import logging
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
proxy_url: str = "http://localhost:8787"
|
||||||
|
port: int = 8025
|
||||||
|
imap_port: int = 11143
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
container_name: "smtp_proxy_server"
|
container_name: "smtp_proxy_server"
|
||||||
ports:
|
ports:
|
||||||
- "8025:8025"
|
- "8025:8025"
|
||||||
|
- "11143:11143"
|
||||||
environment:
|
environment:
|
||||||
- proxy_url=https://temp-email-api.xxx.xxx
|
- proxy_url=https://temp-email-api.xxx.xxx
|
||||||
- port=8025
|
- port=8025
|
||||||
|
- imap_port=11143
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ WORKDIR /app
|
|||||||
COPY requirements.txt /requirements.txt
|
COPY requirements.txt /requirements.txt
|
||||||
RUN python3 -m pip install -r /requirements.txt
|
RUN python3 -m pip install -r /requirements.txt
|
||||||
COPY . /app
|
COPY . /app
|
||||||
ENTRYPOINT [ "python3", "server.py" ]
|
ENTRYPOINT [ "python3", "main.py" ]
|
||||||
|
|||||||
199
smtp_proxy_server/imap_server.py
Normal file
199
smtp_proxy_server/imap_server.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
from twisted.mail import imap4
|
||||||
|
from zope.interface import implementer
|
||||||
|
from twisted.cred.portal import Portal, IRealm
|
||||||
|
from twisted.internet import protocol, reactor, defer
|
||||||
|
from twisted.cred.checkers import ICredentialsChecker, IUsernamePassword
|
||||||
|
|
||||||
|
from config import settings
|
||||||
|
from parse_email import parse_email
|
||||||
|
from models import EmailModel
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(imap4.IMessage)
|
||||||
|
class SimpleMessage:
|
||||||
|
|
||||||
|
def __init__(self, uid=None, email_model: EmailModel = None):
|
||||||
|
self.uid = uid
|
||||||
|
self.email = email_model
|
||||||
|
self.subparts = self.email.subparts
|
||||||
|
|
||||||
|
def getUID(self):
|
||||||
|
return self.uid
|
||||||
|
|
||||||
|
def getHeaders(self, negate, *names):
|
||||||
|
self.got_headers = negate, names
|
||||||
|
return {
|
||||||
|
k.lower(): v
|
||||||
|
for k, v in self.email.headers.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def isMultipart(self):
|
||||||
|
return len(self.subparts) > 0
|
||||||
|
|
||||||
|
def getSubPart(self, part):
|
||||||
|
self.got_subpart = part
|
||||||
|
return SimpleMessage(email_model=self.subparts[part])
|
||||||
|
|
||||||
|
def getBodyFile(self):
|
||||||
|
return BytesIO(self.email.body.encode("utf-8"))
|
||||||
|
|
||||||
|
def getSize(self):
|
||||||
|
return self.email.size
|
||||||
|
|
||||||
|
def getFlags(self):
|
||||||
|
return ["\\Seen"]
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(imap4.IMailboxInfo, imap4.IMailbox)
|
||||||
|
class SimpleMailbox:
|
||||||
|
|
||||||
|
def __init__(self, password):
|
||||||
|
self.password = password
|
||||||
|
self.listeners = []
|
||||||
|
self.addListener = self.listeners.append
|
||||||
|
self.removeListener = self.listeners.remove
|
||||||
|
self.message_count = 0
|
||||||
|
|
||||||
|
def getFlags(self):
|
||||||
|
return ["\\Seen"]
|
||||||
|
|
||||||
|
def getUIDValidity(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def getMessageCount(self):
|
||||||
|
return 2 ** 31 - 1
|
||||||
|
|
||||||
|
def getRecentCount(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def getUnseenCount(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def isWriteable(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getHierarchicalDelimiter(self):
|
||||||
|
return "/"
|
||||||
|
|
||||||
|
def requestStatus(self, names):
|
||||||
|
r = {}
|
||||||
|
if "MESSAGES" in names:
|
||||||
|
r["MESSAGES"] = self.getMessageCount()
|
||||||
|
if "RECENT" in names:
|
||||||
|
r["RECENT"] = self.getRecentCount()
|
||||||
|
if "UIDNEXT" in names:
|
||||||
|
r["UIDNEXT"] = self.getMessageCount() + 1
|
||||||
|
if "UIDVALIDITY" in names:
|
||||||
|
r["UIDVALIDITY"] = self.getUID()
|
||||||
|
if "UNSEEN" in names:
|
||||||
|
r["UNSEEN"] = self.getUnseenCount()
|
||||||
|
return defer.succeed(r)
|
||||||
|
|
||||||
|
def fetch(self, messages, uid):
|
||||||
|
start, end = messages.ranges[0]
|
||||||
|
start = max(start, 1)
|
||||||
|
if start > self.message_count:
|
||||||
|
return []
|
||||||
|
res = requests.get(
|
||||||
|
f"{settings.proxy_url}/api/mails?limit=20&offset={start - 1}", headers={
|
||||||
|
"Authorization": f"Bearer {self.password}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if res.status_code != 200:
|
||||||
|
_logger.error(
|
||||||
|
"Failed: "
|
||||||
|
f"code=[{res.status_code}] text=[{res.text}]"
|
||||||
|
)
|
||||||
|
raise Exception("Failed to fetch emails")
|
||||||
|
if res.json()["count"] > 0:
|
||||||
|
self.message_count = res.json()["count"]
|
||||||
|
return [
|
||||||
|
(start + uid, SimpleMessage(start + uid, parse_email(item["raw"])))
|
||||||
|
for uid, item in enumerate(reversed(res.json()["results"]))
|
||||||
|
]
|
||||||
|
|
||||||
|
def getUID(self, message):
|
||||||
|
return message.uid
|
||||||
|
|
||||||
|
def store(self, messages, flags, mode, uid):
|
||||||
|
# IMailboxIMAP.store
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Account(imap4.MemoryAccount):
|
||||||
|
|
||||||
|
def __init__(self, user, password):
|
||||||
|
self.password = password
|
||||||
|
super().__init__(user)
|
||||||
|
|
||||||
|
def _emptyMailbox(self, name, id):
|
||||||
|
_logger.info(f"New mailbox: {name}, {id}")
|
||||||
|
if name != "INBOX":
|
||||||
|
raise imap4.NoSuchMailbox(name)
|
||||||
|
return SimpleMailbox(self.password)
|
||||||
|
|
||||||
|
def select(self, name, rw=1):
|
||||||
|
return imap4.MemoryAccount.select(self, name)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleIMAPServer(imap4.IMAP4Server):
|
||||||
|
def __init__(self, factory):
|
||||||
|
imap4.IMAP4Server.__init__(self)
|
||||||
|
self.factory = factory
|
||||||
|
|
||||||
|
def lineReceived(self, line):
|
||||||
|
super().lineReceived(line)
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(IRealm)
|
||||||
|
class SimpleRealm:
|
||||||
|
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||||
|
res = json.loads(avatarId)
|
||||||
|
account = Account(res["username"], res["password"])
|
||||||
|
account.addMailbox("INBOX")
|
||||||
|
return imap4.IAccount, account, lambda: None
|
||||||
|
|
||||||
|
|
||||||
|
class IMAPFactory(protocol.Factory):
|
||||||
|
def __init__(self, portal):
|
||||||
|
self.portal = portal
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
p = SimpleIMAPServer(self)
|
||||||
|
p.portal = self.portal
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(ICredentialsChecker)
|
||||||
|
class CustomChecker:
|
||||||
|
credentialInterfaces = (IUsernamePassword,)
|
||||||
|
|
||||||
|
def requestAvatarId(self, credentials):
|
||||||
|
return defer.succeed(json.dumps({
|
||||||
|
"username": credentials.username.decode(),
|
||||||
|
"password": credentials.password.decode(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
def start_imap_server():
|
||||||
|
_logger.info(f"Starting IMAP server on port {settings.imap_port}")
|
||||||
|
portal = Portal(SimpleRealm(), [CustomChecker()])
|
||||||
|
reactor.listenTCP(settings.imap_port, IMAPFactory(portal))
|
||||||
|
reactor.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_logger.info(f"Starting server settings[{settings}]")
|
||||||
|
start_imap_server()
|
||||||
24
smtp_proxy_server/main.py
Normal file
24
smtp_proxy_server/main.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
from smtp_server import start_smtp_server
|
||||||
|
from imap_server import start_imap_server
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
_logger.info(f"Starting server settings[{settings}]")
|
||||||
|
process_list = [
|
||||||
|
multiprocessing.Process(target=start_smtp_server, args=()),
|
||||||
|
multiprocessing.Process(target=start_imap_server, args=()),
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
for p in process_list:
|
||||||
|
p.start()
|
||||||
|
for p in process_list:
|
||||||
|
p.join()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
for p in process_list:
|
||||||
|
p.terminate()
|
||||||
10
smtp_proxy_server/models.py
Normal file
10
smtp_proxy_server/models.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from typing import Dict, List
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class EmailModel(BaseModel):
|
||||||
|
headers: Dict[str, str]
|
||||||
|
body: str
|
||||||
|
content_type: str
|
||||||
|
subparts: List["EmailModel"]
|
||||||
|
size: int
|
||||||
38
smtp_proxy_server/parse_email.py
Normal file
38
smtp_proxy_server/parse_email.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import email
|
||||||
|
from email.message import Message
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from models import EmailModel
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
def get_email_model(msg: Message):
|
||||||
|
subparts = [
|
||||||
|
get_email_model(subpart)
|
||||||
|
for subpart in msg.get_payload()
|
||||||
|
] if msg.is_multipart() else []
|
||||||
|
body = "" if msg.is_multipart() else msg._payload
|
||||||
|
return EmailModel(
|
||||||
|
headers={k: v for k, v in msg.items()},
|
||||||
|
body=body,
|
||||||
|
content_type=msg.get_content_type(),
|
||||||
|
size=len(body) + sum(subpart.size for subpart in subparts),
|
||||||
|
subparts=subparts,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_email(raw: str) -> EmailModel:
|
||||||
|
try:
|
||||||
|
msg = email.message_from_string(raw)
|
||||||
|
return get_email_model(msg)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Could not parse email: {e}")
|
||||||
|
return EmailModel(
|
||||||
|
headers={},
|
||||||
|
body="could not parse email",
|
||||||
|
content_type="text/plain",
|
||||||
|
size=len("could not parse email"),
|
||||||
|
subparts=[],
|
||||||
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
aiosmtpd==1.4.5
|
aiosmtpd==1.4.5
|
||||||
pydantic-settings==2.2.1
|
pydantic-settings==2.2.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
|
twisted==24.3.0
|
||||||
|
|||||||
@@ -3,26 +3,15 @@ import logging
|
|||||||
import email
|
import email
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
from aiosmtpd.controller import Controller
|
from aiosmtpd.controller import Controller
|
||||||
from aiosmtpd.smtp import SMTP, Session, Envelope, AuthResult, LoginPassword
|
from aiosmtpd.smtp import SMTP, Session, Envelope, AuthResult, LoginPassword
|
||||||
|
|
||||||
logging.basicConfig(
|
from config import settings
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
)
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
_logger.setLevel(logging.INFO)
|
_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
proxy_url: str = "http://localhost:8787"
|
|
||||||
port: int = 8025
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = ".env"
|
|
||||||
|
|
||||||
|
|
||||||
class CustomSMTPHandler:
|
class CustomSMTPHandler:
|
||||||
|
|
||||||
def authenticator(self, server, session, envelope, mechanism, auth_data):
|
def authenticator(self, server, session, envelope, mechanism, auth_data):
|
||||||
@@ -119,7 +108,6 @@ class CustomSMTPHandler:
|
|||||||
return '250 OK'
|
return '250 OK'
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
|
||||||
handler = CustomSMTPHandler()
|
handler = CustomSMTPHandler()
|
||||||
server = Controller(
|
server = Controller(
|
||||||
handler,
|
handler,
|
||||||
@@ -133,11 +121,11 @@ server = Controller(
|
|||||||
|
|
||||||
|
|
||||||
async def start():
|
async def start():
|
||||||
_logger.info(f"Starting server settings[{settings}]")
|
_logger.info(f"Starting server on port {settings.port}")
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def start_smtp_server():
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
task = loop.create_task(start())
|
task = loop.create_task(start())
|
||||||
@@ -146,3 +134,8 @@ if __name__ == "__main__":
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
_logger.info("Got KeyboardInterrupt, stopping")
|
_logger.info("Got KeyboardInterrupt, stopping")
|
||||||
server.stop()
|
server.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_logger.info(f"Starting server settings[{settings}]")
|
||||||
|
start_smtp_server()
|
||||||
@@ -123,14 +123,14 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
|||||||
text: '通过 Github Actions 部署',
|
text: '通过 Github Actions 部署',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
{ text: '开发中', link: 'github-action' },
|
{ text: '通过 Github Actions 部署', link: 'github-action' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '附加功能',
|
text: '附加功能',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
{ text: '配置 SMTP 代理服务', link: 'feature/config-smtp-proxy' },
|
{ text: '配置 SMTP IMAP 代理服务', link: 'feature/config-smtp-proxy' },
|
||||||
{ text: '发送邮件 API', link: 'feature/send-mail-api' },
|
{ text: '发送邮件 API', link: 'feature/send-mail-api' },
|
||||||
{ text: '查看邮件 API', link: 'feature/mail-api' },
|
{ text: '查看邮件 API', link: 'feature/mail-api' },
|
||||||
{ text: '配置子域名邮箱', link: 'feature/subdomain' },
|
{ text: '配置子域名邮箱', link: 'feature/subdomain' },
|
||||||
|
|||||||
BIN
vitepress-docs/docs/public/feature/imap.png
Normal file
BIN
vitepress-docs/docs/public/feature/imap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -1,10 +1,10 @@
|
|||||||
# 搭建 SMTP 代理服务
|
# 搭建 SMTP IMAP 代理服务
|
||||||
|
|
||||||
## 为什么需要 SMTP 代理服务
|
## 为什么需要 SMTP IMAP 代理服务
|
||||||
|
|
||||||
SMTP 的应用场景更加广泛
|
`SMTP` `IMAP` 的应用场景更加广泛
|
||||||
|
|
||||||
## 如何搭建 SMTP 代理服务
|
## 如何搭建 SMTP IMAP 代理服务
|
||||||
|
|
||||||
### Local Run
|
### Local Run
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ cd smtp_proxy_server/
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
./venv/bin/python3 -m pip install -r requirements.txt
|
./venv/bin/python3 -m pip install -r requirements.txt
|
||||||
./venv/bin/python3 server.py
|
./venv/bin/python3 main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Run
|
### Docker Run
|
||||||
@@ -32,10 +32,21 @@ docker-compose up -d
|
|||||||
services:
|
services:
|
||||||
smtp_proxy_server:
|
smtp_proxy_server:
|
||||||
image: ghcr.io/dreamhunter2333/cloudflare_temp_email/smtp_proxy_server:latest
|
image: ghcr.io/dreamhunter2333/cloudflare_temp_email/smtp_proxy_server:latest
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: dockerfile
|
||||||
container_name: "smtp_proxy_server"
|
container_name: "smtp_proxy_server"
|
||||||
ports:
|
ports:
|
||||||
- "8025:8025"
|
- "8025:8025"
|
||||||
|
- "11143:11143"
|
||||||
environment:
|
environment:
|
||||||
- proxy_url=https://temp-email-api.xxx.xxx
|
- proxy_url=https://temp-email-api.xxx.xxx
|
||||||
- port=8025
|
- port=8025
|
||||||
|
- imap_port=11143
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 使用 Thunderbird 登录
|
||||||
|
|
||||||
|
下载 [Thunderbird](https://www.thunderbird.net/en-US/)
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
Reference in New Issue
Block a user