admin管理员组

文章数量:1531356

**

之前项目系统中包含了一个邮箱下载模块,其中对接的是腾讯企业邮箱,这个模块前后也维护了不短时间,想写下这篇文章来聊聊具体问题,如果有需要对接腾讯企业邮箱的需求,同时官方给予的开发文档无法满足需求,希望本篇文章可以作为参考。
网络上有不少对邮箱进行开发的文章,但都简略的太多了,远不能处理实际情况。
文章也说明了腾讯企业邮箱开发中的很多坑。

**

先谈谈前提吧

  1. 需求是需要从邮箱中下载所有的数据(数据用于分析等等,whatever,总之需要down下来
  2. 腾讯企业邮箱给开发者们提供了详细的企业邮开发文档,但是存在一个问题,想要按照腾讯企业邮的开发规范来开发邮箱,必须获取企业邮箱的最高权限,即开发文档中称之为CorpIDCorpSecret的密钥(or账户密码?)。如果不是希望搭建公司内部的企业邮箱管理,这一开发规范是存在安全问题的。例如张三需要对其中某一个或多个邮箱进行开发,而却需要得到企业邮的最高权限,这对于企业管理者来说是不能容忍的。至此,这一文章就为处理这一局限而来。

基于什么开发?

  1. 腾讯企业邮箱
  2. python(2.7)
  3. Linux(and file system
  4. imap协议(RFC 3501、RFC 822
  5. 腾讯提供信息:邮箱服务器信息

让我们开始吧

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. 明确我们需要下载什么?
  1. 发件人
  2. 收件人
  3. 抄送人
  4. 邮件时间
  5. 正文内容
  6. 附件

这是作为普通邮箱使用者,在看到一封邮件时,所能看到的数据信息,但是实际情况有所出入。
在一份实际邮件当中,其实应该有如下信息:

  1. 发件人
  2. 收件人
  3. 抄送人
  4. 邮件发件时间
  5. 邮件收件时间
  6. 正文内容
  7. 邮件途径的服务器信息
  8. 附件

对比两者的差异在于邮件时间邮件途径的服务器信息,邮件时间对于普通用户来说用户所看到的其实是发件时间,而邮件途径的服务器信息,这一数据对于普通用户而言,又有什么用处呢?所以自然也就没有展示给用户了。

就这些了吗?远不如此,电子邮件的诸多国际协议中对于邮件数据还规定了非常多的规范,例如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

其实腾讯的企业邮真的烂的一批,反正对于邮箱协议的支持很烂很烂,比如不能根据时间请求这一类命令的支持……还有挺多问题的,比如超过多大附件就会出问题等等等等等等等啊啊啊啊

就这样吧,没人看就先放着了

本文标签: 腾讯企业邮箱方式文档