得快点。快速的IMAP电子邮件同步

你好!我是伊利亚两年前,我加入了IMAP移动客户端。该应用程序的早期版本长时间下载了信件列表,并花费大量流量来更新邮箱。出现了关于优化协议工作以及该协议的功能的问题。我对协议一无所知,因此投入阅读文档。事实证明,客户一直在使用协议的过程中一直没有间断,并且根本没有考虑到实现功能。这些功能帮助将邮件下载速度提高了2到3倍。关于IMAP是什么,以及在稍后的文章中对其进行优化的芯片是什么。

我不会深入研究该协议。而不是“我想两年前读这篇文章”类别的文章。IMAP专家不太可能为自己找到新信息。本文依赖于RFC 3501中的协议描述

服务器连接


IMAP是一种有状态协议。这对我来说是一个发现,在此之前我还没有看到或使用过这些协议。考虑使用服务器的方案。 


让我们按顺序进行,最重要的是,结合示例。首先,您需要创建与服务器的连接。为此,请使用openSSL库。

openssl s_client -connect imap.server.com:993 -crlf 

很好,建立了连接,您可以通过以CAPABILITY响应开头的行来观察OK响应

OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE  SPECIAL-USE AUTH=PLAIN AUTH=LOGIN]

每个CAPABILITY都有一个方便的备忘单,其中所有可能的CAPABILITY值都通过RFC链接编写。例如,IMAP4rev1告诉客户端服务器正在按照IMAP4标准工作,并且IDLE信号通知您可以订阅邮箱中发生的更改。

服务器授权


连接到服务器后,您需要转到邮箱。这是使用LOGIN命令完成的。

a1 LOGIN email pass

所以,停止,登录,我明白了,这是什么?-也许你问。这是团队标签。为了客户的利益,标签应该是不同的,因为响应到达时带有与请求相同的标签,这意味着可以将其匹配以在团队之间进行解析。服务器还可以返回开头带有星号的响应,例如* OK,这称为未标记响应。基本上,对于希望在响应中包含多个实体的团队,例如LIST,将返回这样的答案。 

文件夹列表请求


要请求文件夹中的字母列表,必须首先找到这些文件夹。这是通过LIST命令完成的。此命令返回服务器上的文件夹列表。

A2 LIST «» *
* LIST (\HasNoChildren \Trash) «/» Trash
* LIST (\HasNoChildren \Sent) «/» Sent
* LIST (\HasNoChildren \Drafts) «/» Drafts
* LIST (\HasNoChildren \Junk) «/» Junk
* LIST (\HasNoChildren) «/» INBOX
A2 OK List completed (0.001 + 0.000 + 0.001 secs).

命令中的第一个参数是名称空间。如果服务器支持名称空间,则可以使用NAMESPACE查询请求其值。标准名称空间看起来像一个空字符串。接下来,通配符参数起作用。有了它,我们可以告诉服务器我们需要返回哪些文件夹。例如,我们可以得到:文件夹树分支,仅根,或仅所有,如上例所示。最好不要这样做,因为谁知道用户框中有多少个文件夹。该协议的作者建议使用“%”-在这种情况下,您将从邮箱中获得所有顶级文件夹。 

根据答案,我们知道这是一个未标记的答案,其中每一行都是框中的文件夹。首先,有一些标记可用来读取文件夹的元信息,例如,在该示例中,所有文件夹都没有后代,而某些特殊用途的文件夹(如“废纸,”,“垃圾”等)也是如此。接下来是文件夹分隔符。此符号用于子文件夹。例如,对于“垃圾箱”文件夹的后代,名称看起来像“垃圾箱/新文件夹”。在所有文件夹之后,服务器将使用分配给该命令的标签以及该命令的执行时间向我们返回“确定”。  

资料夹选择


进一步根据该方案,我们必须选择一个文件夹,从中将收紧我们的消息。这是使用SELECT命令完成的。

4 SELECT INBOX
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent \*)] Flags permitted.
* 16337 EXISTS
* 2 RECENT
* OK [UNSEEN 6037] First unseen.
* OK [UIDVALIDITY 1532079879] UIDs valid
* OK [UIDNEXT 17412] Predicted next UID
* OK [HIGHESTMODSEQ 21503] Highest
4 OK [READ-WRITE] Select completed (0.015 + 0.000 + 0.014 secs).

选择文件夹后,将返回有关该文件夹的所有信息。让我们去吧。

  • 回答文件夹中允许包含字母的标志。  
  • 用标志回答客户可以永远改变
  • 回复文件夹中的字母数
  • 答案是最近的信件数量,即我们在选择文件夹之间收到的信件数量
  • 回复未读邮件数

好吧,现在,让我们来谈谈。我们不需要的其余信息。

要求信件


现在,最有趣的是请求字母。您在这里必须格外小心,尤其是在移动客户端上。同意,当您输入应用程序时,不太可能会收到从服务器到数据库的数千条消息。此外,下载整个字母没有意义,因为例如显示所有字母的列表可能不切实际。例如,为了快速显示用户字母,我们仅要求提供“信封”。在此信封中,我们希望看到:发件人,收件人,信件主题和发送日期。我们将加载前10个帖子。

5 FETCH 16337:16327 (ENVELOPE)

冒号列举了我们要接收的字母数量的一部分,并在括号中括了我们要从这些字母中读取的内容,在这种情况下,是该字母的信封。

我将以缩写形式给出答案:

* 16334 FETCH (ENVELOPE ("Sat, 07 Sep 2019 23:07:48 +0000" "Hello from Fabric.io" (("Fabric" NIL "notifier" "fabric.io")) (("Fabric" NIL "notifier" "fabric.io")) (("Fabric" NIL "notifier" "fabric.io")) ((NIL NIL "me" "me@mail")) NIL NIL NIL "<5d7438441b07c_2d872ad30967b9646405c6@answers-notifier2012.mail>"))

显然,没有什么是清楚的。事实是,信封格式由RFC 2822规定。在本文中我将不考虑它。此信封包含所有必要的信息:信件的接收日期,信件的主题,发件人,收件人,甚至messageId。他的客户用来显示对话。

因此,我们能够向用户显示有关字母的基本信息,但是正文呢?
无论大小如何,我们都可以立即下载该信函的全文,这虽然时间不长,但是在网络和内存上却很昂贵。顺便说一句,这是使用相同的FETCH命令完成的。 

6 FETCH 16337:16327 (BODY[]) 

尝试在收件箱中使用这样的命令,您将了解我所说的“昂贵”的意思,即使收到10条消息,我们也会收到相当多的答复,其中包含有关该信函的所有信息。说起她。

您在已知的任何客户端中多久下载一次该信的来源,以查看其原始形式?如果没有,让我们从中得到一封测试信。在其中,我直接在信件上添加了图片,并在附件中添加了图片。将其保存为eml格式,然后使用任何文本编辑器将其打开。根据客户的不同,您会收到不同的来信来源,但总的来说它们是相似的。 

让我们从电子邮件标题开始:

Return-Path: <myemail>
Delivered-To:myemail
Received: from localhost (localhost [127.0.0.1])
	byimap.server.com (imap.server.com) with ESMTP id 6C2BE2A0363
	for <myemail>; Sun,  8 Sep 2019 23:41:29 +0300 (MSK)
X-Virus-Scanned: amavisd-new at imap.server.com
Received: from imap.server.com ([127.0.0.1])
	by localhost ( imap.server.com [127.0.0.1]) (amavisd-new, port 10026)
	with ESMTP id abx8HQQT_k5A for <myemail>;
	Sun,  8 Sep 2019 23:41:29 +0300 (MSK)
Mime-Version: 1.0
Date: Sun, 08 Sep 2019 20:41:28 +0000
Content-Type: multipart/mixed;
 boundary=»--=_Part_722_554093397.1567975288»
Message-ID: <9e4e3872e603eac2c20f26bb1d65548d>
From: "Me" <myemail>
Subject: Hey, Habr!
To: myemail
X-Priority: 3 (Normal)

所有元信息都在信件的标题中描述,从谁,向谁,何时,邮件内容的类型,信件的主题和优先级。边界字段指示字母的边界。

进一步了解这意味着什么。

----=_Part_722_554093397.1567975288
Content-Type: multipart/related;
 boundary=»--=_Part_583_946112260.1567975288»
----=_Part_583_946112260.1567975288
Content-Type: multipart/alternative;
 boundary=»--=_Part_881_599167713.1567975288»
----=_Part_881_599167713.1567975288
Content-Type: text/plain; charset=«utf-8»
Content-Transfer-Encoding: quoted-printable
----=_Part_881_599167713.1567975288
Content-Type: text/html; charset=«utf-8»
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE html><html><head><meta http-equiv=3D"Content-Type" content=3D"t=
ext/html; charset=3Dutf-8" /></head><body><div data-crea=3D"font-wrapper"=
 style=3D«font-family: XO Tahion; font-size: 16px; direction: ltr»> <img =
src=3D"cid:jua-uid-q1nz1guinitrcfd3-1567975257318"><br><br><div></div> <b=
r> </div></body></html>
----=_Part_881_599167713.1567975288--
----=_Part_583_946112260.1567975288
Content-Type: image/jpeg; name=«2018-09-04 22.46.36.jpg»
Content-Disposition: inline; filename=«2018-09-04 22.46.36.jpg»
Content-ID: <jua-uid-q1nz1guinitrcfd3-1567975257318>
Content-Transfer-Encoding: base64

每个边界都是作品的通常边界。它们以两个连字符“-”开头。右边框的末尾有这两个连字符。RFC1341对此进行了更详细的描述,

这可以称为字母的主要部分,此处描述了字母的各个部分及其MIME类型。

关于MIME类型
MIME- , MIME (Multipurpose Internet Mail Extensions) email . 

  • multipart/mixed , . 

  • multipart/related , , , 

  • multipart/alternative , , , text/plain text/html, . 


我们这里没有简单的文本,因此采用html表示法更合逻辑。在此html演示文稿中,只有一幅带有Content-Disposition:内联参数的图片,也就是说,它直接位于信函正文中,而不位于附件中。

此图片的链接不是很简单。它由Content-ID参数描述,该参数等于jua-uid-q1nz1guinitrcfd3-1567975257318。这是到字母下一部分的链接-图片以base-64编码。为了避免烦恼,我没有包含所有的base-64代码

,这封信的最后一部分是 

----=_Part_722_554093397.1567975288
Content-Type: image/png; name=«2018-07-02 11.08.23 pm.png»
Content-Disposition: attachment; filename=«2018-07-02 11.08.23 pm.png»
Content-Transfer-Encoding: base64

已经具有Content-Disposition而不是内联的(如上图所示),但是具有附件。该图像应该只进入文件附件面板,因为它也以base-64编码并且尺寸较大。在这里可以清楚地看到,如果我们只想显示基本信息,则不应再次加载该信函的整个正文。 

返回协议


处理字母后,您需要关闭选定的文件夹并与服务器说再见。要关闭文件夹,我们需要输入CLOSE命令。是的,很简单


7 CLOSE
7 OK Close completed (0.001 + 0.000 secs).

顺便说一句,如果您与我同时使用控制台并阅读了这篇文章,那么可能会发生不太愉快的事件,服务器可能会因超时而关闭您的连接。这是完全正常的,每个服务器都有自己的超时时间,例如,我们有30分钟。 
因此,建议在后台执行NOOP命令

1 NOOP
1 OK NOOP completed (0.001 + 0.000 secs).

它实际上不执行任何操作,但是允许您在不超时的情况下根据需要保持连接。如果当前选择一个文件夹,则NOOP可以作为对此文件夹进行更改的定期请求 

1 NOOP
* 16472 EXPUNGE
* 16471 EXPUNGE
* 16472 EXISTS
* 1 RECENT
1 OK NOOP completed (0.004 + 0.000 + 0.003 secs).

在此响应中,我们收到两则已删除邮件的通知,其中一则是新邮件,该文件夹中的邮件数量为16472。

我还注意到,您只能使用一个选定的文件夹,此处没有并行工作。

好了,最后,关闭与服务器的会话,我们将告别它。

8 LOGOUT
* BYE Logging out
8 OK Logout completed (0.001 + 0.000 secs).

我们看到了悲伤的,未加标签的BYE答案,这意味着是时候完成工作了。

与CONDSOTORE和QRESYNC快速同步


您可以使用NOOP操作来跟踪所选文件夹中一个框中的更改。但是,如果我们要查找与另一个文件夹一起工作时文件夹中发生了什么变化,该怎么办?最明显的选择是对本地存储中的所有字母(包括缓存或数据库)进行排序,然后与服务器返回的内容进行比较。一方面,这确实是一种解决方案,在某些服务器上,这实际上是唯一的解决方案。另一方面,我们希望以协议通常允许的速度显示字母。幸运的是,我们的服务器支持协议扩展,例如CONDSTORE和QRESYNC,这些扩展已添加到RFC7162中。。第一个在邮件和文件夹中添加一个特殊的63位数字,称为mod序列,该数字随对该字母的每次操作而增加。所有消息中最高的mod序列被添加到该文件夹​​中。结果,每次您连接到支持CONDSTORE的服务器上的文件夹时,我们只需比较本地和服务器文件夹的mod-sequence值,就可以轻松找出是否已更改。

另外,此扩展为STORE和FETCH命令添加了其他参数-CHANGEDSINCE mod-sequence和UNCHANGEDSINCE mod-sequence,如果所传输消息的mod-sequence分别大于或小于此值,则可以执行操作。让我们看一个例子。

FETCH 17221:17241 (UID) (CHANGEDSINCE 0)
* OK [HIGHESTMODSEQ 22746] Highest
* 17222 FETCH (UID 18319 MODSEQ (22580))
* 17223 FETCH (UID 18320 MODSEQ (22601))
* 17224 FETCH (UID 18324 MODSEQ (22607))
* 17225 FETCH (UID 18325 MODSEQ (22604))
* 17226 FETCH (UID 18326 MODSEQ (22608))
* 17227 FETCH (UID 18327 MODSEQ (22614))
* 17228 FETCH (UID 18328 MODSEQ (22613))
* 17229 FETCH (UID 18336 MODSEQ (22628))
* 17230 FETCH (UID 18338 MODSEQ (22628))
* 17231 FETCH (UID 18340 MODSEQ (22628)
* 17232 FETCH (UID 18341 MODSEQ (22628))
* 17221 FETCH (UID 18318 MODSEQ (22583))

我模拟了一种情况,我们进入邮箱之前对此一无所知,即我们的本地mod序列为0。如您所见,服务器通常向我们返回邮箱中的所有消息,因为在此之前我们没有收到任何消息对盒子一无所知为了响应CHANGEDSINCE对UID字母的请求,我们将保存一个HIGHESTMODESEQ并进行无标记的响应OK,现在将其保存,对于每个消息,我们的MODSEQ。

我们将对邮箱执行一些操作:添加新字母,更改标志。让我们发出一个新请求,但使用以前的mod序列

1 fetch 17221:* (UID FLAGS) (CHANGEDSINCE 22746)
* 17267 FETCH (UID 18378 FLAGS () MODSEQ (22753))
* 17270 FETCH (UID 18381 FLAGS (\Seen) MODSEQ (22754))
* 17271 FETCH (UID 18382 FLAGS () MODSEQ (22751))
* 17273 FETCH (UID 18384 FLAGS () MODSEQ (22750))

并且我们已经看到了区别,而不是输出刚刚到达的20个新旧社区(17221中的星号:*表示将字母从17221最大化)接收到的MODSEQ大于上一个。这对于同步一个我们已经有一段时间没有使用的文件夹并获得对已更改字母的转换的帮助非常有用,而不是尝试所有可能的字母。

看起来好多了?但是QRESYNC使同步操作更加快速,它允许您在选择文件夹时指定MODSEQ参数和我们已知的消息UID。让我们用一个例子来解释。首先,必须使用ENABLE命令启用QRESYNC。 

1 ENABLE QRESYNC
* ENABLED QRESYNC
1 OK Enabled (0.001 + 0.000 secs).
1 SELECT INBOX (QRESYNC (0 0))
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent \*)] Flags permitted.
* 17271 EXISTS
* 0 RECENT
* OK [UNSEEN 17241] First unseen.
* OK [UIDVALIDITY 1532079879] UIDs valid
* OK [UIDNEXT 18385] Predicted next UID
* OK [HIGHESTMODSEQ 22754] Highest
1 OK [READ-WRITE] Select completed (0.001 + 0.000 secs).

由于之前我们对该文件夹一无所知,因此服务器仅向我们返回有关该文件夹的信息,而没有对其进行任何更改。假设我们询问了前20条消息,并记住了它们的UID和HIGHESTMODESEQ。我们离开文件夹,向自己发送消息,删除消息,更改标志并返回有关该文件夹的过去信息

1 CLOSE
1 OK Close completed (0.001 + 0.000 secs).
1 SELECT INBOX (QRESYNC (1532079879 22754 18300:18385))
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent \*)] Flags permitted.
* 17271 EXISTS
* 0 RECENT
* OK [UNSEEN 17241] First unseen.
* OK [UIDVALIDITY 1532079879] UIDs valid
* OK [UIDNEXT 18386] Predicted next UID
* OK [HIGHESTMODSEQ 22757] Highest
* VANISHED (EARLIER) 18380
* 17269 FETCH (UID 18383 FLAGS () MODSEQ (22757))
* 17271 FETCH (UID 18385 FLAGS () MODSEQ (22755))
1 OK [READ-WRITE] Select completed (0.001 + 0.000 secs).

现在,当选择一个已更改的文件夹时,我们会立即收到一些更改,对已删除的邮件的响应消失(EARLIER),对于已添加或更改的消息则响应FETCH。现在,如果用户长时间不访问文件夹,则同步文件夹甚至更加容易。如果您在缓存中本地存储了一堆消息,并且不想将它们与服务器上的消息进行比较,这是一种非常酷的方法。

此请求的第一个参数是UIDVALIDITY,该参数实际上用于验证您先前收到的uid在文件夹中没有更改。如果服务器将所有消息的会话uid从一个会话更改为另一个会话,或者该文件夹被删除并且在其位置创建了一个具有相同名称的文件夹,则可能发生这种情况。

第二个参数是我们已知的HIGHESTMODSEQ,最后一个是已知UID的间隔,如果间隔是连续的,或者可以用逗号分隔,它们可以写为冒号。

结论


在我的示例中,我遇到一种情况,即对主题区域的无知会导致应用程序的错误和次佳操作。在本文中,我没有涵盖使用协议的所有可能选项。但我希望对于IMAP客户端的下一个开发人员来说,以上信息对您有所帮助。

IMAP有很多有趣的东西。用于快速同步的命令仅仅是开始,实际上,您可以根据服务器的功能进一步优化不同的IMAP命令,并使处理邮件的速度更快,在网络和内存上更为经济,并且通常更为舒适。但是我稍后会讨论。

All Articles