admin管理员组文章数量:1531356
**
之前项目系统中包含了一个邮箱下载模块,其中对接的是腾讯企业邮箱,这个模块前后也维护了不短时间,想写下这篇文章来聊聊具体问题,如果有需要对接腾讯企业邮箱的需求,同时官方给予的开发文档无法满足需求,希望本篇文章可以作为参考。
网络上有不少对邮箱进行开发的文章,但都简略的太多了,远不能处理实际情况。
文章也说明了腾讯企业邮箱开发中的很多坑。
**
先谈谈前提吧
- 需求是需要从邮箱中下载所有的数据(数据用于分析等等,whatever,总之需要down下来
- 腾讯企业邮箱给开发者们提供了详细的企业邮开发文档,但是存在一个问题,想要按照腾讯企业邮的开发规范来开发邮箱,必须获取企业邮箱的最高权限,即开发文档中称之为
CorpID
和CorpSecret
的密钥(or账户密码?)。如果不是希望搭建公司内部的企业邮箱管理,这一开发规范是存在安全问题的。例如张三需要对其中某一个或多个邮箱进行开发,而却需要得到企业邮的最高权限,这对于企业管理者来说是不能容忍的。至此,这一文章就为处理这一局限而来。
基于什么开发?
- 腾讯企业邮箱
- python(2.7)
- Linux(and file system
- imap协议(RFC 3501、RFC 822
- 腾讯提供信息:邮箱服务器信息
让我们开始吧
1. python内置库对于imap协议进行了支持,有一个库名为imaplib
# Step 1. 建立连接
# 编写一个连接类,在初始化时就进行登录
# 根据腾讯给提供的接收邮件服务器信息,设值默认端口和服务器地址
#
import imaplib
class ESCnn(imaplib.IMAP4_SSL):
# Email Server Connection
def __init__(self, user, password,
port=993, host="imap.exmail.qq"):
imaplib.IMAP4_SSL.__init__(self, host, port)
self.email_user = user
self.email_password = password
self.login(user=self.email_user, password=self.email_password) # 创建对象时就进行登录处理
2. 明确我们需要下载什么?
- 发件人
- 收件人
- 抄送人
- 邮件时间
- 正文内容
- 附件
这是作为普通邮箱使用者,在看到一封邮件时,所能看到的数据信息,但是实际情况有所出入。
在一份实际邮件当中,其实应该有如下信息:
- 发件人
- 收件人
- 抄送人
- 邮件发件时间
- 邮件收件时间
- 正文内容
- 邮件途径的服务器信息
- 附件
对比两者的差异在于邮件时间、邮件途径的服务器信息,邮件时间对于普通用户来说用户所看到的其实是发件时间,而邮件途径的服务器信息,这一数据对于普通用户而言,又有什么用处呢?所以自然也就没有展示给用户了。
就这些了吗?远不如此,电子邮件的诸多国际协议中对于邮件数据还规定了非常多的规范,例如Message-Header、RFC2821协议、RFC2822协议……,但!咱们就此打住,这已经足够我们的需求了。如果要展开那得是好几本书才能说清楚的。
我们明确了需要在下载什么,那么是不是可以给邮件进行一个封装类了?
等等,还差一点东西。
我们需要知道邮件对于一个邮箱账户来说是如何管理的。那么多邮件,在服务器内部是如何确定其唯一性的呢?
UID:
邮箱服务器通过UID来确定邮件的唯一性,邮箱中有不同的文件夹,例如收件箱、垃圾箱、草稿箱和一些自定义的文件夹,而UID的唯一性是针对这些文件夹来说的,每一个文件夹中都有一个唯一的UID来对邮件进行标记,即便被删除了也不会复用这一UID,当然,这是协议所规定的,至于落实到具体的服务器实现那就是另一回事了,也当然作为邮箱服务器的开发者需要遵循这一标准。记住是UID不是Message-ID,这两者是有区别的,在此不展开说明了,有兴趣可以打开以下链接资料自行了解。
🔗Message-ID wiki🔗what-is-the-difference-between-imapmessage-getuid-and-message-id-header stackoverflow
那么,至此,我们可以对一个邮件进行数据封装了。
# Step 2. 邮件(单个)信息管理
_TimeFormat = "%Y-%m-%d %H:%M:%S"
_DateFormat = _TimeFormat[:8]
class RecvMail:
def __init__(self, ):
self._box = None # 邮件所属文件夹名称
self._uid = None # UID
self._re0()
def _re0(self):
self._attachments = [] # 附件信息
self._mail_subject = None # 邮件标题
self._mail_recv_time = None # 邮件接收时间
self._mail_send_time = None # 邮件发送时间
self._mail_text = [] # 邮件正文内容
self._mail_sender = None # 邮件发送邮箱用户
self._mail_cc = set() # 邮件抄送邮箱用户
self._mail_to = set() # 邮件目的邮箱用户
self._antetype = None # 邮件原始数据
self._set_to_invoke = 0
self._set_cc_invoke = 0
@property
def send_date(self):
assert self._mail_send_time is not None
return self._mail_send_time.strftime(_DateFormat)
@property
def send_time(self):
assert self._mail_send_time is not None
return self._mail_send_time.strftime(_TimeFormat)
@property
def recv_date(self):
assert self._mail_recv_time is not None
return self._mail_recv_time.strftime(_DateFormat)
@property
def recv_time(self):
assert self._mail_recv_time is not None
return self._mail_recv_time.strftime(_TimeFormat)
@property
def attachements(self):
return self._attachments
@property
def uid(self)
assert self._uid is not None
return self._uid
@property
def box(self):
assert self._box is not None
return self._box
@property
def antetype(self):
return self._antetype
@property
def sender(self)
assert self._mail_sender is not None
return self._mail_sender
@property
def receivers(self):
assert self._set_to_invoke != 0
return list(self._mail_to)
@property
def cc(self):
assert self._set_cc_invoke != 0
return list(self._mail_cc)
@property
def set_box(self,box):
self._re0()
self._box = box
self._uid = None
@property
def set_uid(self, uid):
self._re0()
self._uid = uid
@property
def set_subject(self, subject):
self._mail_subject = subject
@property
def set_to(self, to):
self._mail_to.update(to)
# 因为我们需要确保这一数据是被解析处理过了,所以使用了这一变量记录
self._set_to_invoke += 1
@property
def set_cc(self, cc)
self._mail_cc.update(cc)
# 因为我们需要确保这一数据是被解析处理过了,所以使用了这一变量记录
self._set_cc_invoke += 1
@property
def set_send_time(self, datetime)
self._mail_send_time = datetime
@property
def set_recv_time(self, datetime):
self._mail_recv_time = datetime
def mount(self, mail_message):
self._antetype = mail_message
def append_mailtext(self, content):
# 用于存储邮件正文数据(后续代码会告诉你为什么要用append
self._mail_text.append(content)
def append_attachment(self, attach, filename):
# 用于存储邮件附件数据(附件肯定是多个的,如果有的话,所以封装在list中
self._attachment.append(
{"filename": filename,
"raw": attach}
)
def get_common_attrs(self):
return {
"mail_box": self.box,
"mail_uid": self.uid,
"mail_sender": self.sender,
"mail_subject": self.subject,
"mail_cc": self.cc,
"mail_to": self.receivers,
"mail_send_time": self.send_time,
"mail_send_date": self.send_date,
"mail_recv_time": self.recv_time,
"mail_recv_date": self.recv_date,
}
3. 我们如何下载?
至此,我们已经有了和邮箱之间的连接,也明确了邮件需要下载什么。
那么到了这里就是如何去下载一封邮件了。
class EMCtrl(object):
# Email Message Controller
def __init__(self, ecnn):
self._re0()
self._ecnn = ecnn
self._carrier = RecvMail()
@property
def ECnn(self):
return self._ecnn
def _re0(self):
self._workfor_box = None
self._workfor_uid = None
def get_boxes_list(self):
status, boxes_list = self._ecnn.list()
if status == 'OK':
return [box.split('"/"')[-1].replace('"', '').strip()
for box in boxes_list]
else:
# error handle or something
return []
def select(self, mailbox='INBOX', readonly=False):
status, _ = self._ecnn.select(mailbox, readonly)
if status == 'OK':
self._workfor_box = mail_box
self._carrier.set_box = (mail_box)
else:
# error handle or something
pass
def get_uids(self, box=None):
if box is not None:
self.select(box)
# 这一步需要对RFC3501协议比较清楚才能知道为什么这么处理,这关乎于协议规定的命令规范,由于篇幅比较长在此不过多解释,有兴趣可以自行搜索RFC3501协议阅读
# 但是也要说明为什么使用"1:*"这个命令参数,由于腾讯企业邮箱的残废协议支持,只有这一方式可以靠谱地获取完整的UID信息
status, raw_uids = self._ecnn.uid('SEARCH', '1:*')
if status == 'OK':
return raw_uids[0].split()
else:
# error handle or something
pass
def query_mail(self, uid):
# 参数说明请参阅RFC822协议,这一操作用于获取邮件数据
status, mail_raw_message = self._ecnn.uid('FETCH', uid, '(RFC822)')
if not ((status=='OK') and
(mail_raw_message is not None) and
(mail_raw_message[0] is not None)):
# 参数说明请参阅RFC3501 FETCH命令章节部分,由于RFC822的支持问题获取不到该数据而采用RFC3501协议规定方式
status, mail_raw_message = self._ecnn.fetch(uid, '(BODY.PEEK[])')
if not ((status=='OK') and
(mail_raw_message is not None) and
(mail_raw_message[0] is not None)):
# error handle or something
pass
return
email_message = EMCtrl._convert_raw_message(mail_raw_message[0][1])
self._carrier.set_uid(uid)
self._carrier.mount(email_message)
return self._carrier.antetype # type: email.message.Message
@staticmethod
def _convert_raw_message(raw_message):
# 具体作用请参阅email.message_from_string
return email.message_from_string(raw_message)
def _get_mail_times(self):
# get_all 为获取Message邮件头部信息中的所有相关字段数据
# 而received为邮件所到达的服务器所添加的头数据
# 有几个received数据就说明中间经过几个邮箱服务器
received_fields = self._carrier.antetype.get_all('received' [])
if received_fields:
# 邮箱服务器所提供的时间数据的解析
mailtime_tuples = [
email.uitils.parsedate(
re.sub(parttern='[\t\r\n]',
repl='',
string=recv.split(';')[-1].strip(),
)[:31]
)
for recv in received_fields
]
# 对此排序我们就可以得知发件时间和收件时间分别是什么
mailtime_tuples.sort()
else:
# 对于头部信息中date字段数据其实是非常不可靠的,除非万不得已,尽可能考虑邮箱服务器所提供的时间信息
mailtime_tuple = [email.utils.parsedate(self._carrier.antetype.get('date'))]
def get_mail_from(self):
mail_from = self._carrier.antetype.get('from')
mail_from = parseaddr(mail_from)[-1])
self._carrier.set_sender(mail_from)
return self._carrier.sender
eeeeeeeeeeeeeeee
不想写了,懒癌发作,有时间再更新了,或者有人想要帮助再写
后续还有比较重要的一部分是如何解析正文内容和附件内容,其中正文内容结构体比较杂乱,需要长时间维护才知道会遇到哪些情况……ending
其实腾讯的企业邮真的烂的一批,反正对于邮箱协议的支持很烂很烂,比如不能根据时间请求这一类命令的支持……还有挺多问题的,比如超过多大附件就会出问题等等等等等等等啊啊啊啊
就这样吧,没人看就先放着了
版权声明:本文标题:腾讯企业邮箱开发(非官方开发文档方式 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dongtai/1725784782a1042555.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论