Tenho que ir rápido. Sincronização rápida de email IMAP

Olá! Eu sou Ilya. Dois anos atrás, entrei para o cliente móvel IMAP. As versões anteriores do aplicativo baixavam a lista de cartas por um longo tempo e gastavam muito tráfego para atualizar a caixa de correio. Surgiu a questão de otimizar o trabalho com o protocolo e sobre as capacidades desse protocolo em geral. Eu não sabia nada sobre o protocolo e mergulhei na leitura da documentação. Acontece que, durante todo esse tempo, o cliente usou o protocolo sem interrupções e não levou em conta os recursos de implementação. Esses recursos ajudaram a acelerar o download de e-mails em 2 a 3 vezes. Sobre o que é IMAP e quais são os chips para otimizá-lo mais adiante no meu artigo.

Não vou mergulhar profundamente no protocolo. Um artigo da categoria "Gostaria de ler este artigo há dois anos". É improvável que os gurus do IMAP encontrem novas informações por si mesmos. Este artigo se baseia na descrição do protocolo da RFC 3501 .

Conexão com o servidor


IMAP é um protocolo stateful. Essa foi uma descoberta para mim, antes que eu não tinha visto ou trabalhado com esses protocolos. Considere o esquema de trabalhar com o servidor. 


Vamos em ordem, e mais importante, com exemplos. Primeiro, você precisa criar uma conexão com o servidor. Para fazer isso, use a biblioteca openSSL.

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

Ótimo, a conexão foi estabelecida e você pode observar a resposta OK com uma linha que começa com a resposta CAPABILITY

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

Há uma cábula prática para cada CAPABILITY , onde todos os valores possíveis de CAPABILITY são gravados com links para o RFC. Por exemplo, IMAP4rev1 informa ao cliente que o servidor está funcionando de acordo com o padrão IMAP4 e IDLE sinaliza que você pode assinar as alterações que ocorrem na caixa de correio.

Autorização do servidor


Depois de se conectar ao servidor, você precisa ir para a sua caixa de correio. Isso é feito usando o comando LOGIN.

a1 LOGIN email pass

Então, pare, faça o login, eu entendo, e a1 o que é isso? - Talvez você pergunte. E esta é a etiqueta da equipe. No interesse do cliente, as tags devem ser diferentes, pois a resposta chega com a mesma tag da solicitação, o que significa que ela pode ser correspondida para análise entre equipes. O servidor também pode retornar uma resposta com um asterisco no início, como * OK, isso é chamado de resposta sem marcação. Basicamente, essa resposta é retornada para equipes que esperam várias entidades na resposta, por exemplo, LIST. 

Solicitação de lista de pastas


Para solicitar uma lista de letras em uma pasta, você deve primeiro descobrir essas pastas. Isso é feito pelo comando LIST. Este comando retorna uma lista de pastas no servidor.

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).

O primeiro parâmetro no comando é namespace. Se o servidor suportar espaço para nome, seus valores poderão ser solicitados usando a consulta NAMESPACE. O espaço para nome padrão se parece com uma sequência vazia. Em seguida, o parâmetro curinga entra em jogo. Com isso, podemos dizer ao servidor quais pastas precisamos retornar. Por exemplo, podemos obter: um ramo de árvore de pastas, apenas raízes ou tudo, como no exemplo acima. É melhor não fazer isso, porque quem sabe quantas pastas o usuário tem na caixa. Os autores do protocolo recomendam o uso de "%" - nesse caso, você obterá todas as pastas de nível superior da caixa de correio. 

A partir da resposta, entendemos que essa é uma resposta sem marcação em que cada linha é sua pasta na caixa. Primeiro, há sinalizadores pelos quais lemos as meta-informações da pasta, por exemplo, no exemplo, todas as pastas não têm descendentes e algumas pastas para fins especiais (como Lixo, Lixo, etc.). Em seguida, vem o caractere separador de pastas. Este símbolo é usado para subpastas. Por exemplo, para um descendente da pasta Lixeira, o nome seria semelhante a "Lixeira / Nova Pasta". Depois de todas as pastas, o servidor retornará OK para nós com a tag que atribuímos ao comando e o tempo de execução desse comando.  

Seleção de pasta


Além disso, de acordo com o esquema, devemos selecionar uma pasta na qual iremos apertar nossas mensagens. Isso é feito usando o comando 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).

Quando você seleciona uma pasta, todas as informações sobre ela são retornadas. Vamos em ordem.

  • Responda com sinalizadores permitidos dentro da pasta para obter letras.  
  • Responda com sinalizadores que o cliente pode mudar para sempre
  • Responda com o número de letras na pasta
  • A resposta é com o número de cartas recentes, ou seja, aquelas que recebemos entre as seleções de pasta
  • Responder com o número de mensagens não lidas

Bem, por enquanto, vamos nos debruçar sobre isso. O restante das informações não precisamos.

Solicitar cartas


Agora, o mais interessante é o pedido de cartas. Você precisa ser extremamente cuidadoso aqui, especialmente em clientes móveis. Concordo, é improvável que, ao entrar no aplicativo, você receba milhares de mensagens do servidor para o seu banco de dados. Além disso, não faz sentido fazer o download da carta inteira, pois pode não ser prático exibir, por exemplo, uma lista de todas as letras. Por exemplo, para mostrar rapidamente as cartas do usuário, solicitaremos apenas um "envelope". Neste envelope, queremos ver: remetente, destinatário, assunto da carta e data do envio. Carregaremos as 10 primeiras postagens.

5 FETCH 16337:16327 (ENVELOPE)

Os dois pontos enumeram o segmento dos números de letras que queremos receber e entre parênteses o que queremos ler dessas cartas, nesse caso, o envelope da carta.

Vou dar a resposta de forma abreviada:

* 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>"))

É claro que nada está claro. E o fato é que o formato do envelope é ditado pela RFC 2822. Não vou considerá-lo neste artigo. Este envelope possui todas as informações necessárias: data de recebimento da carta, assunto da carta, remetente, destinatário e até mesmo messageId. Seus clientes costumam exibir uma conversa.

Assim, conseguimos mostrar ao usuário informações básicas sobre a carta, mas e o corpo?
Podemos fazer o download imediato de todo o corpo da carta, independentemente de seu tamanho, é claro que não é por muito tempo, mas ainda assim caro pela rede e memória. A propósito, isso é feito com o mesmo comando FETCH. 

6 FETCH 16337:16327 (BODY[]) 

Tente esse comando na sua caixa de entrada e você entenderá o que eu quis dizer com “caro”, mesmo com 10 mensagens, obtemos uma resposta bastante volumosa com absolutamente todas as informações sobre a carta. Falando dela.

Com que frequência você baixou a fonte da carta em qualquer cliente que você conhece para ver sua aparência na sua forma original? Caso contrário, vamos obter uma carta de teste. Nele, adicionei uma figura diretamente à carta e uma figura como anexo. Salve-o no formato eml e abra-o com qualquer editor de texto. Dependendo do cliente, você receberá diferentes fontes da carta, mas em geral elas serão semelhantes. 

Vamos começar com o cabeçalho do email:

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)

Todas as meta-informações são descritas no cabeçalho da carta, de quem, para quem, quando, tipo de conteúdo da mensagem, assunto e prioridade da carta. O campo limite indica o limite da letra.

Entenda melhor o que isso significa.

----=_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

Cada limite é a borda usual de um pedaço de escrita. Eles começam com dois hífens "-". A borda de fechamento possui esses dois hífens no final. É descrito em mais detalhes no RFC1341.

Isso pode ser chamado de parte principal da carta, partes da carta e seus tipos MIME são descritos aqui.

Sobre tipos MIME
MIME- , MIME (Multipurpose Internet Mail Extensions) email . 

  • multipart/mixed , . 

  • multipart/related , , , 

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


Não temos texto simples aqui, por isso é mais lógico usar uma representação html. Nesta apresentação em html, há apenas uma figura com o parâmetro Content-Disposition: inline, ou seja, ele está localizado diretamente no corpo da carta e não nos documentos anexos.

O link para esta imagem não é muito simples. É descrito pelo parâmetro Content-ID, que é igual a jua-uid-q1nz1guinitrcfd3-1567975257318 . Este é um link para a próxima parte da carta - uma figura codificada na base 64. Para salvar meus nervos, não incluí todo o código da base 64.

A última parte da carta tem a forma 

----=_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

que já possui Disposição de conteúdo não embutida, como na imagem acima, mas anexo. Essa imagem deve apenas ir para o painel de anexo de arquivo, pela maneira como também é codificada na base-64 e tem um tamanho grande. Aqui fica claro que você não deve carregar novamente todo o corpo da carta se quisermos mostrar apenas informações básicas. 

Voltar ao protocolo


Depois de trabalhar nas letras, você precisa fechar a pasta selecionada e dizer adeus ao servidor. Para fechar a pasta, precisamos inserir o comando CLOSE. Sim, é tão simples


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

A propósito, se você trabalhou com o console em paralelo comigo e leu o artigo, um evento não tão agradável poderia ter acontecido, o servidor poderia fechar sua conexão com o tempo limite. Isso é completamente normal e cada servidor tem seu próprio tempo limite, por exemplo, temos 30 minutos. 
Portanto, é recomendável executar o comando NOOP em segundo plano

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

Ele literalmente não faz nada, mas permite que você mantenha a conexão sem tempo limite, tanto quanto precisamos. Se você atualmente selecionar uma pasta, o NOOP poderá funcionar como uma solicitação periódica de alterações nessa pasta 

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

Aqui, na resposta, somos notificados de duas mensagens excluídas, uma nova e que o número de mensagens nesta pasta é 16 472.

Observe também que você pode trabalhar com apenas uma pasta selecionada, não há trabalho paralelo aqui.

Bem, no final, feche a sessão com o servidor e nos despediremos.

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

Vemos a triste resposta sem sinal BYE, o que significa que é hora de terminar o trabalho.

Sincronização rápida com CONDSOTORE e QRESYNC


Você pode usar a operação NOOP para rastrear alterações em uma caixa em uma pasta selecionada. Mas e se quisermos descobrir o que mudou na pasta enquanto estávamos trabalhando com outra? A opção mais óbvia é classificar todas as letras no armazenamento local, seja um cache ou um banco de dados, e comparar com o que o servidor retornará. Por um lado, essa é realmente uma solução e, em alguns servidores, será literalmente a única verdadeira. Por outro lado, queremos mostrar letras o mais rápido que o protocolo geralmente permitir. Felizmente, nosso servidor suporta extensões de protocolo como CONDSTORE e QRESYNC, que foram adicionadas ao RFC7162. O primeiro adiciona um número especial de 63 bits à mensagem e pasta, chamada de sequência mod, que aumenta a cada operação nesta carta. A sequência de mod mais alta entre todas as mensagens é adicionada à pasta. Como resultado, cada vez que você se conecta a uma pasta em um servidor que suporta o CONDSTORE, podemos facilmente descobrir se algo mudou ou não, simplesmente comparando os valores da sequência de mod para as pastas local e do servidor.

Além disso, esta extensão adiciona parâmetros adicionais para os comandos STORE e FETCH - CHANGEDSINCE mod-sequence e UNCHANGEDSINCE mod-sequence, que permitem executar uma operação se a sequência mod das mensagens transmitidas for maior e menor que isso, respectivamente. Vejamos um exemplo.

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))

Simulei uma situação em que entramos na caixa de correio e não sabíamos nada sobre isso antes, ou seja, nossa sequência de modificação local é 0. Como você pode ver, o servidor nos retorna geralmente todas as mensagens que estão na caixa de correio, pois antes disso não recebíamos nada e não sabe nada sobre a caixa. Em resposta a uma solicitação de cartas UID de CHANGEDSINCE, uma resposta não marcada OK também vem com um HIGHESTMODESEQ que agora salvaremos e para cada mensagem nosso MODSEQ.Executaremos

algumas operações com a caixa de correio: adicione novas cartas, altere os sinalizadores. Vamos fazer uma nova solicitação, mas com a sequência de modificação anterior

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))

e já vemos a diferença, em vez de gerar 20 comunidades novas e antigas que acabamos de chegar (asterisco em 17221: * significa levar letras do número 17221 ao máximo possível), recebemos cartas cujo MODSEQ é maior que o anterior. Isso ajuda muito bem a sincronizar uma pasta na qual não estamos há algum tempo e obter uma espécie de conversão das letras alteradas, em vez de tentar todas as possíveis.

Parece, muito melhor? Mas o QRESYNC torna a operação de sincronização ainda mais rápida, permitindo especificar os parâmetros MODSEQ e as UIDs de mensagens conhecidas por nós durante a seleção da pasta. Vamos explicar com um exemplo. Primeiro, o QRESYNC deve ser ativado com o comando ENABLE. 

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).

como não sabíamos nada sobre a pasta antes disso, o servidor retorna apenas informações sobre a pasta para nós, sem um nugget de suas alterações. Suponha que tenhamos perguntado as primeiras vinte mensagens e lembrado o UID e também o HIGHESTMODESEQ. Saímos da pasta, nos enviamos uma mensagem, excluímos a mensagem, alteramos os sinalizadores e retornamos com as informações anteriores sobre a pasta

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).

E agora, ao escolher uma pasta alterada, obtemos imediatamente um nugget de alterações, na forma de uma resposta VANISHED (EARLIER) para mensagens que foram excluídas e FETCH para mensagens que foram adicionadas ou alteradas. Agora é ainda mais fácil sincronizar a pasta se o usuário não a visitar há um longo tempo. Essa é uma maneira muito interessante se você tiver várias mensagens armazenadas localmente no cache e não quiser compará-las com as mensagens no servidor.

O primeiro parâmetro dessa solicitação é UIDVALIDITY, que é essencialmente usado para verificar se o uid que você recebeu anteriormente não foi alterado na pasta. Isso pode acontecer se o servidor alterar o uid da sessão de sessão para sessão para todas as mensagens ou se a pasta foi excluída e uma pasta com o mesmo nome foi criada em seu lugar.

O segundo parâmetro é o HIGHESTMODSEQ conhecido por nós e o último é o intervalo de UIDs conhecidos. Eles podem ser gravados como dois pontos, se o intervalo for contínuo ou separado por vírgula.

Conclusão


No meu exemplo, deparei-me com uma situação em que a ignorância da área de assunto leva à operação incorreta e subótima do aplicativo. Não cobri todas as opções possíveis para usar o protocolo neste artigo. Mas espero que para o próximo desenvolvedor do cliente IMAP as informações acima sejam úteis.

O IMAP tem uma tonelada de coisas interessantes. Os comandos para sincronização rápida são apenas o começo; na verdade, você pode otimizar ainda mais os comandos IMAP, dependendo dos recursos do servidor, e tornar o trabalho com o correio mais rápido, mais econômico na rede e na memória e geralmente mais agradável. Mas vou falar sobre isso mais tarde.

All Articles