mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-07 05:42:50 +08:00
252 lines
7.2 KiB
Python
252 lines
7.2 KiB
Python
import json
|
|
import logging
|
|
import httpx
|
|
|
|
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 generate_email_model, 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"]
|
|
|
|
def getInternalDate(self):
|
|
return self.email.headers.get("Date", "Mon, 1 Jan 1900 00:00:00 +0000")
|
|
|
|
|
|
@implementer(imap4.IMailboxInfo, imap4.IMailbox)
|
|
class SimpleMailbox:
|
|
|
|
def __init__(self, name, password):
|
|
self.name = name
|
|
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 self.message_count or 1000
|
|
|
|
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.getUIDValidity()
|
|
if "UNSEEN" in names:
|
|
r["UNSEEN"] = self.getUnseenCount()
|
|
return defer.succeed(r)
|
|
|
|
def fetch(self, messages, uid):
|
|
if self.name == "INBOX":
|
|
return self.fetchINBOX(messages)
|
|
if self.name == "SENT":
|
|
return self.fetchSENT(messages)
|
|
return []
|
|
|
|
def fetchINBOX(self, messages):
|
|
start, end = messages.ranges[0]
|
|
start = max(start, 1)
|
|
limit = min(20, end - start + 1) if end and end >= start else 20
|
|
if self.message_count > 0 and start > self.message_count:
|
|
return []
|
|
res = httpx.get(
|
|
f"{settings.proxy_url}/api/mails?limit={limit}&offset={start - 1}",
|
|
headers={
|
|
"Authorization": f"Bearer {self.password}",
|
|
"x-custom-auth": f"{settings.basic_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(res.json()["results"])
|
|
]
|
|
|
|
def fetchSENT(self, messages):
|
|
start, end = messages.ranges[0]
|
|
start = max(start, 1)
|
|
limit = min(20, end - start + 1) if end and end >= start else 20
|
|
if self.message_count > 0 and start > self.message_count:
|
|
return []
|
|
res = httpx.get(
|
|
f"{settings.proxy_url}/api/sendbox?limit={limit}&offset={start - 1}",
|
|
headers={
|
|
"Authorization": f"Bearer {self.password}",
|
|
"x-custom-auth": f"{settings.basic_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, generate_email_model(item)))
|
|
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
|
|
raise NotImplementedError
|
|
|
|
|
|
class Account(imap4.MemoryAccount):
|
|
|
|
def __init__(self, user, password):
|
|
self.password = password
|
|
super().__init__(user)
|
|
|
|
def isSubscribed(self, name):
|
|
return name.upper() in ["INBOX", "SENT"]
|
|
|
|
def _emptyMailbox(self, name, id):
|
|
_logger.info(f"New mailbox: {name}, {id}")
|
|
if name == "INBOX":
|
|
return SimpleMailbox(name, self.password)
|
|
if name == "SENT":
|
|
return SimpleMailbox(name, self.password)
|
|
raise imap4.NoSuchMailbox(name.encode("utf-8"))
|
|
|
|
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):
|
|
# _logger.info(f"Received: {line}")
|
|
super().lineReceived(line)
|
|
|
|
def sendLine(self, line):
|
|
# _logger.info(f"Sent: {line}")
|
|
super().sendLine(line)
|
|
|
|
|
|
@implementer(IRealm)
|
|
class SimpleRealm:
|
|
def requestAvatar(self, avatarId, mind, *interfaces):
|
|
res = json.loads(avatarId)
|
|
account = Account(res["username"], res["password"])
|
|
account.addMailbox("INBOX")
|
|
account.addMailbox("SENT")
|
|
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()
|