4 de novembro de 2011

Trabalhando com emails em uma caixa postal usando IMAP4 em Delphi

Mostrei num outro post aqui no blog como ter acesso aos emails numa caixa postal através do protocolo POP3 no Delphi. O POP3 é um protocolo amplamente adotado para permitir o acesso remoto aos emails de uma caixa postal mas ele é bastante limitado. Seus recursos estão restritos basicamente à manipulação das mensagens existentes na caixa postal, permitindo lê-las, apagá-las ou arquivá-las.

Se precisar de recursos mais avançados - tais como gerenciar pastas, localizar uma mensagem pesquisando o texto dela, compartilhar caixas postais com um grupo de trabalho, entre outras - temos que apelar para o IMAP4, um protocolo mais completo.

O primeiro passo para trabalhar com o IMAP4, assim como acontece com os outros protocolos baseados no TCP/IP, é estabelecer uma conexão com o servidor. Aqui também isso exige que alguns parâmetros sejam informados, tais como o endereço do servidor, a porta onde ele estará aguardando requisições e um usuário com permissão de acesso ao serviço, bem como sua respectiva senha:
var iMap TIdIMAP4;
begin
iMap := TIdIMAP4.Create (Nil);

{ Atribui os dados fornecidos pelo usuário e tenta se conectar }
iMap.Host := _ServerIMAP; { Endereço do servidor }
iMap.Port := _PortaIMAP; { Porta que o servidor está ouvindo }
iMap.Username := _UserIMAP;
iMap.Password := _SenhaIMAP;

iMap.Connect(100000);

{ Realiza aqui algumas tarefas com o IMAP conectado}

{ Descarta a conexão e outros recursos qdo não forem mais necessários }
iMap.Disconnect();

iMap.Free ();
end;
Uma grande diferença em relação ao funcionamento do POP3 é que o IMAP4 pode gerenciar várias caixas postais numa única conexão. Do ponto de vista do usuário, cada caixa postal é normalmente encarada como uma pasta. Como os comandos do IMAP4 são direcionados a uma caixa postal, isso nos obriga a selecionar uma antes de submeter os comandos.

Ao menos a caixa chamada INBOX (caixa de entrada) deve existir mas você pode levantar uma lista completa com todos os nomes das caixas que existem na conta usando a função ListMailBoxes. Um usuário pode, então, ser apresentado a essa lista para que escolha a caixa mais apropriada para as necessidades dele. Considerando o iMap criado e conectado no quadro anterior, fazer o levantamento é bem simples:
var lMaixBoxes : TStringList;
lCaixaSel : String;
begin
{ ... }
lMaixBoxes := TStringList.Create();

iMap.ListMailBoxes(lMaixBoxes);

{ Carrega a lista na tela para o usuário escolher uma delas. }
lCaixaSel := SelecionaCaixaPostal (lMaixBoxes);

lMailBoxes.Free();

if (not iMap.SelectMailBox (lCaixaSel) ) then
raise Exception.Create('Erro na seleção da caixa postal');
{ ... }
end;
Uma vez que a caixa foi escolhida, precisamos repassá-la ao iMap para que possamos operar com a caixa específica. É o que faz a chamada à função SelectMailBox no código acima. Essa chamada faz com que o TidIMAP4 alimente sua propriedade MailBox com um resumo do estado atual da caixa selecionada, disponibilizando informações como a quantidade total de mensagens e quantas dessas permanecem não lidas, entre outras informações.

Com uma caixa selecionada, podemos realizar operações como pesquisar os emails contidos nela. Nesse quesito, o IMAP4 é muito mais flexível que o POP3 já que nos fornece uma opção para filtrar as mensagens desejadas usando a estrutura TIdIMAP4SearchRec. O principal recurso dessa estrutura é o campo SearchKey. É esse campo que determina quais critérios devem ser respeitados na consulta e, portanto, quais dos outros campos da estrutura serão considerados quando a consulta for submetida pela função SearchMailBox. O exemplo abaixo usa o iMap conectado para procurar todas as mensagens que ainda não foram lidas na caixa postal atual e varre a lista encontrada, recuperando cada uma delas.
var rec : array of TIdIMAP4SearchRec;
TheMsg : TIdMessage;
contMsg, i, idMsg : Integer;
begin
{ Procura por emails do tipo skUnseen - ou seja, somente os "não lidos" }
SetLength(rec,1);
rec[0].SearchKey := skUnseen;
if not iMap.SearchMailBox (rec) then
raise Exception.Create('Erro na pesquisa da caixa postal');

{ Quantas mensagens foram encontradas ? }
contMsg := iMap.MailBox.SearchResult.Length;

for i := 0 to contMsg - 1 do begin
TheMsg := TIdMessage.Create(Nil);

{ Obtem a identificação da mensagem ... }
idMsg := iMap.MailBox.SearchResult[i];

{ ... e a usa para recuperar o email completo }
iMap.Retrieve (idMsg, TheMsg);

{ Analisa anexos e outros procedimentos }
end;
end;
Veja que a a função de pesquisa aceita um array de filtros como parâmetro. Isso significa que podemos construir uma combinação de filtros para que uma lista mais refinada seja retornada. Outras possibilidades de filtros para pesquisar a caixa incluem o texto contido nas mensagens ou seus cabeçalhos, a data de recepção delas ou até mesmo seu tamanho.

Um outro ponto importante a destacar é que o resultado da pesquisa é armazenado na propriedade SearchResult do MailBox. O resultado fica acessível como uma lista de identificadores das mensagens que atenderam o critério estipulado para a busca. Esses identificadores são números inteiros que podem ser usados em diversas funções da caixa postal, como a que recupera o conteúdo da mensagem ou que permite excluí-la.

Uma vez que uma mensagem foi recuperada com sucesso, a instância da classe TIdMessage nos dá acesso a todas as informações pertinentes a ela, incluindo os anexos, caso ela possua algum. O trecho que trata os anexos foi removido pra não poluir muito o quadro. Como a recuperação de anexos funciona exatamente igual ao do POP3, o processo descrito no post Recuperando emails de uma caixa postal com Delphi pode ser usado para realizar esse trabalho.

Um outro recurso do IMAP4 é que você pode copiar mensagens da caixa atualmente selecionada pra uma outra caixa usando a função CopyMsgs. Ela aceita 2 parâmetros : um array com os identificadores das mensagens a serem copiadas e o nome da caixa postal que receberá as mensagens. Caso precise mover as mensagens, use essa função combinada com a DeleteMsgs, que exclui uma lista de mensagens da caixa atual.

O IMAP4 ainda disponibiliza funções para gerenciar as caixas postais. É possível criar uma caixa nova, renomer uma caixa e excluir caixas existentes. Os nomes das funções com esses objetivos são bastante intuitivos : CreateMailBox, DeleteMailBox e RenameMailBox.

25 comentários :

renato.adm@live.com disse...

Cara,

to com uma duvida aqui,

no servidor as minhas pastas tem acentuação e cedilha e quando utilizo a função para ListMailBox ele traz a string bagunçada exemplo:

No servidor minha pasta é: Anotações e o retorno da lista é Anota&AOcA9Q-es

sacou? vc tem alguma ideia de como resolver isso?

Luís Gustavo Fabbro disse...

Renato

A questão que vc apontou acontece porque a comunicação com o servidor IMAP tem que respeitar a codificação UTF-7. A grosso modo, essa codificação mantem intactos os caracteres ASCII de um texto enquanto certos caracteres unicode são "expandidos" para que possam ser representados como uma sequência de caracteres ASCII.

Por exemplo, no texto "Notificação" os caracteres ç e ã não são ASCII e, por isso, são expandidos respectivamente como &AOc- e &AOM- de modo que todo o texto é representado usando apenas ASCII.

Você pode decodificar os nomes de suas pastas e apresentá-los de forma legível ao seu usuário. Só não se esqueça de decodificar esse nome de novo quando for submeter a seleção ao servidor pois este exige que o nome respeite o UTF7.

No Wikipedia (link http://en.wikipedia.org/wiki/UTF-7) há uma descrição dos algorítmos para codificar e decodificar textos com UTF-7.

[]

renato.adm@live.com disse...

Fantástico! Luís,

resolveu o meu problema.

Eu fiz uma gambiarra aqui assim:
Usei um servidor apache + php que conectava ao imap e retornava o email em xml (usando o base64 encode do PHP eu codificava as strings sem perder a codificação real. Em seguida eu usei o Delphi para fazer essa importação do xml e consequentemente decodificar com o base64 decode.

De certa forma posso utilizar este "web service" em outro processo, mas fico dependendo do servidor web. A melhor solução neste momento é a que você explicou.

Li também sobre a codificação UTF-7 e aprendi um pouco mais sobre esses mapeamentos.

Gostaria de parabenizar pelo post explicativo, acredito que vai ajudar muita gente por ai.

valeu!

Hélio Galvão disse...

gente, eu fiz um programa aqui na empresa mas eu gostaria que o mesmo aproveitasse as configuracoes do outlook para enviar uma mensagem por email (Usamos servidor exchenge aqui) para alguns endereços internos.

Gilberto Caetano disse...

Boa noite Luís

Como faço para setar uma mensagem como Lida ou não Lida (seria UnSeen e Seen)?

Abraços

Luís Gustavo Fabbro disse...

Gilberto

Pra modificar as flags de uma (ou mais) mensagem(ns), use a função StoreFlags da classe TidIMAP4. Com essa função você pode, por exemplo, incluir ou remover a marca mfSeen, o que sinaliza à caixa postal se a mensagem foi ou não lida pelo usuário.

[]s

Gilberto Caetano disse...

Luís Gustavo,

Sua dica foi muito importante para resolver esse "problema", que estava me deixando estressado. :)
Aqui tá o miolo do que fiz para remover o flag mfSeen (visto) baseado na sua explicação:
.
.
Exclude(Flags, mfSeen);
iMap.StoreFlags(idMsg, sdReplace, Flags);
.
.

Obrigado! Abraços

Anônimo disse...

Luís Gustavo,

Sua dica foi muito boa, obrigado! Mas estou tendo problema com acentuação também, no
iMap.ListMailBoxes(lMaixBoxes);

Estou usando o Delphi 7 e procurei informações de como converter o UTF7 para o string no delphi e não consegui.

Pode me dar mais essa dica ?

Luís Gustavo Fabbro disse...

Há no Wikipedia um artigo sobre a codificação UTF-7, incluindo um trecho sobre algorítimos de codificação/decodificação e links para outros documentos mais completos sobre o assunto. O link é http://en.wikipedia.org/wiki/UTF-7.

Em Delphi, a classe TStrings e suas heranças possuem uma propriedade Encoding para auxiliar nessa conversão. O valor TUTF7Encoding permite trabalhar com textos codificados em UTF-7. Há um exemplo na documentação online da Embarcadero.

Não sei se essa propriedade está disponível no Delphi 7...

[]s

Luís Gustavo Fabbro disse...

Hélio

Dê uma olhada no post Integrando o Outlook em aplicações Delphi - Parte II. Ele trata do envio de email através de automação com o Outook.

[]s

Unknown disse...

como faço para me conectar o servidor da hotmail?
o "pop3.live.com" -> Porta 995,110. Não funciona!

Erro: Connection Closed Grasefully

Utilizo o Delphi 10

Luís Gustavo Fabbro disse...

Walisson

Não tenho certeza absoluta mas acho que o Hotmail não trabalha com IMAP. Neste endereço há um pequeno comparativo feito pela Microsoft para auxiliar na escolha entre GMail e Hotmail; uma das condições listadas para se escolher o GMail é que este permite conexões IMAP e, consequentemente, te deixa trabalhar com outros Clients de email (inclusive um criado por vc mesmo).

[]s

Unknown disse...

Host: imap.gmail.com
Porta: 993
conexao Segura (SSL)

Configurei o cliente mas ele tenta se conectar e trava, li em um site que é preciso usar conexao segura, será este o problema?

Luís Gustavo Fabbro disse...

Walisson

Ao usar esse host, você estará trabalhando com email do GMail (e não mais do Hotmail). Portanto, deve informar login e senha de uma conta do GMail.

Esse host exige que se trabalhe com conexão segura; desse modo, você terá que fazer algumas configurações extras. Há algumas dicas de como usar conexão segura neste link, similar ao que é discutido para envio de emails neste outro link.

Você precisará também das bibliotecas do OpenSSL corretas para a versão do Delphi/Indy na qual vc está desenvolvendo.

[]s

Unknown disse...

Luis, as suas dicas são ótimas, mas estou com um pequeno problema ao tentar fazer a cópia da mensagem. Ao invés de retornar True, sempre me retorna false, o problema é que não sei onde pode estar errado.

Segue abaixo o trecho com o código.

var
TheUID : string;
idMsgs : array of Integer;
begin
TheUID := lvHeaders.Selected.SubItems.Strings[3];
SetLength(idMsgs, 30);
idMsgs[0] := StrToInt(TheUID);
if IdIMAP41.CopyMsgs(idMsgs, cbbPastas.Text) then
begin
ShowMessage('copiado');
end
else
begin
ShowMessage('Email não copiado');
end;


end;

Luís Gustavo Fabbro disse...

Francisco

Experimente ajustar em 1 o comprimento do array dos identificadores das mensagens a serem recuperadas. Talvez a cópia esteja retornando FALSE porque apenas a primeira posição contém um ID válido. Ex:

SetLength(idMsgs, 1);
idMsgs[0] := StrToInt(TheUID);
if IdIMAP41.CopyMsgs(idMsgs, cbbPastas.Text) then
ShowMessage('copiado')
else
ShowMessage('Email não copiado');

[]s

Fernando Medeiros disse...

Muito bom o artigo.
Seria interessante colocar as bibliotecas que devemos colocar em USES.

Infoco Soluções Visuais disse...

Ola Luis Bom Dia.
Depois de semanas trabalhando conseguimos implementar a aplicação graças as suas dicas, dentro das nossas necessidades. Porém gostaria de uma nova ajuda. Quando eu pego o conteudo de TIDAttachemnt gostaria de separar os anexos "reais" dos não "Reais". Exemplo, Quando o arquivo tem assinatura com imagem, eu to salvando isso no banco que pra mim não serve de nada, apenas preciso do que realmente é valido. Resumindo, quero eliminar qualquer imagem de assinatura. É possivel? Mais uma vez obrigado

Luís Gustavo Fabbro disse...

Até onde eu sei, não há uma marcação indicando qual arquivo foi diretamente anexado pelo usuário e qual é parte de uma assinatura (ou outra decoração do email).

Na ABC71, nós carregamos automaticamente os XML com notas fiscais recebidas por email pelos Clientes. Para detectar qual dos anexos é um XML, nós analisamos o conteúdo deles.

Vc pode ainda verificar se o nome do arquivo aparece no corpo do HTML que representa a mensagem do email; o nome de um anexo incluído pelo usuário não costuma ser referenciado no HTML.

[]s

Infoco Soluções Visuais disse...

Ola Boa tarde, consegui resolver esse problema pegando o código HTML mesmo. Agora pra fechar o esquema aqui to precisando setar o email para NÂO LIDO caso ocorra algum problema, tem alguma sugestão pra me ajudar? muito obrigado mais uma vez amigo

FSDownlaodBR disse...

Olá

Parabéns pelo seu blog ele é muito bom, muito legal você passar o seu conhecimento para nós desde iniciantes até avançados em delphi.

Eu estou desenvolvendo um programa que varra todos os "Enviados" do Gmail, para que eu consiga
encontrar arquivos de NFE em .xml, so que eu estou com problemas na hora de dizer ao IMAP que
eu quero que so selecione os "Enviados" do email..

Neste código abaixo so me retorna falso, debuguei e verifiquei que ele so retorna "INBOX", o que eu posso fazer
para solucionar o caso. E qual parametro eu posso passar ao IMAP para verificar se aquele email tem ou não arquivo
.xml.

Código--> if Imap4.SelectMailBox('[Gmail]/Sent Mail') then

Desde já eu te agradeço
Atenciosamente
Ilich Lênin

Luís Gustavo Fabbro disse...

Pra modificar as flags de uma (ou mais) mensagem(ns), use a função StoreFlags da classe TidIMAP4. Com essa função você pode, por exemplo, incluir ou remover a marca mfSeen, o que sinaliza à caixa postal se a mensagem foi ou não lida pelo usuário.

[]s

Infoco Soluções Visuais disse...

Mais uma vez muito obrigado!!

Luís Gustavo Fabbro disse...

Ilich

Usei a função ListMailBoxes do TIdIMAP4 com minha conta do GMail e o nome da caixa para mensagens enviadas apareceu traduzida : "Gmail]/E-mails enviados".

Com esse nome, acessei diretamente a caixa sem problemas. Talvez vc deva incluir em seu programa uma opção para permitir o usuário selecionar a caixa com a qual ele quer trabalhar, mostrando-lhe a lista retornada pelo ListMailBoxes.

[]s

Thiago Recla disse...

É possível efetuar uma pesquisa por data específica?
Já tentei de todas as formas e não consegui

Seguindo seu exemplo, tentei da seguinte forma, mas não deu certo.
rec[0].SearchKey := skSince;
rec[0].Date := date;

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.