quinta-feira, 4 de agosto de 2011

Recuperando emails de uma caixa postal com Delphi

Até pouco tempo atrás, o ERP da ABC71 possuia um recurso que permitia aos usuários enviarem sugestões de melhorias do software por email. Uma aplicação em nossos servidores monitorava a caixa postal específica, interpretava algumas informações incluídas em cada email, vinculava a solicitação a um responsável e respondia automaticamente o email do usuário com uma mensagem pré-formatada preparada pelo atendimento a Clientes.

Hoje esse esquema não existe mais - foi substituído por outros canais com papel similar - mas a ideia da aplicação capaz de monitorar uma caixa postal ainda é válida e pode ser reaproveitada em outros cenários.

A solução adotada por nós é baseada no protocolo chamado POP3 (Post Office Protocol). Com ele é possível recuperar todas as mensagens existentes na caixa de entrada de uma conta de email. A implementação dele que usamos é a do Projeto Indy distribuida junto com o IDE do Delphi;

Da mesma forma que outros protocolos baseados no IP, usar o POP3 exige que se faça a conexão com o servidor antes de realizar qualquer operação. Temos, então, que informar o nome (ou endereço) desse servidor, a porta onde se conectar e um nome de usuário e sua senha, válidos para a operação desejada.
var _IdPOP3 : TIdPOP3;
begin
_IdPOP3 := TIdPOP3.Create (Nil);

_IdPOP3.Host := _ServidorPOP;
_IdPOP3.Port := _PortaPOP;
_IdPOP3.Username := _Username;
_IdPOP3.Password := _Userpwd;
_IdPOP3.AutoLogin := True;

_IdPOP3.Connect ();

A propriedade AutoLogin ajustada com o valor true indica que o componente tentará automaticamente fazer o login no servidor sempre que isso for necessário, evitando que tenhamos que comandar o login manualmente.

Uma vez que temos a conexão estabelecida, podemos iniciar a recuperação das mensagens da caixa de entrada. Há basicamente dois níveis de informação que conseguimos recuperar sobre uma mensagem. Em ambos os casos, as informações são armazenadas numa variável do tipo TIdMessage. No primeiro nível, são obtidos somente os dados de cabeçalho da mensagem - como o assunto, o remetente e os destinatários -, o que reduz o tráfego na rede. O trecho abaixo recupera o cabeçalho da primeira mensagem da caixa de entrada:
var msg : TidMessage;
begin
msg := TidMessage.Create(self);

_IdPOP3.RetrieveHeader (1, msg);
{ ... }
msg.Free;

O segundo nível permite recuperar a mensagem completa, incluindo todas as informações do nível anterior, bem como o corpo da mensagem e todos os anexos dela. Ao utilizar esse método, a caixa postal marca automaticamente a mensagem como lida:
var msg : TidMessage;
begin msg := TidMessage.Create(self);

_IdPOP3.Retrieve (1, msg);
{ ... }
msg.Free;

Com as informações obtidas nesse nível, temos maior flexibilidade para customizar tratamentos para as mensagens. Por exemplo, podemos redirecionar a mensagem para outra pessoa de acordo com o conteúdo ou processar os anexos, salvando-os numa pasta do computador ou num banco de dados.

Não podemos esquecer que o conteúdo de um email pode ser composto de diversas partes distintas, conforme mostrei no post sobre envio de email com Delphi. Então, dependendo de como o email foi composto, recuperar o corpo da mensagem pode ser um simples acesso à propriedade msg.Body.Text ou significará termos que percorrer a lista de partes para determinar qual é o texto contido. Se o texto no corpo está em branco, basta percorrer a lista de partes procurando por uma que seja do tipo texto (TIdText):
var { ... }
parte : TIdMessagePart;
texto : TIdText;
corpo : string;
begin
{ ... }
_IdPOP3.Retrieve(1, msg);

for j := 0 to msg.MessageParts.Count - 1 do begin
parte := msg.MessageParts.Items[j];
{ Verifica se a parte é um texto }
if (parte is TIdText) then begin
texto := parte as TIdText;
corpo := texto.Body.Text;
break;
end;
{ ... }

O tipo do texto contido numa parte fica informado na propriedade ContentType. Como é permitido ter mais que uma parte com texto, pode-se testar esse tipo para recuperar o texto que interessa. É comum enviar uma parte com texto HTML e outra com texto TEXT/PLAIN para que o programa leitor de email decida qual exibir, de acordo com a opção feita pelo usuário.

Da mesma forma que recuperamos o texto lendo as partes, podemos salvar os anexos. Para isso, a parte deve ser do tipo TIdAttachment. Também podemos determinar o tipo do conteúdo do anexo lendo a propriedade ContentType da parte em questão. Salvar o arquivo fica fácil :
{ ... }
for j := 0 to msg.MessageParts.Count - 1 do begin
parte := msg.MessageParts.Items[j];
{ Verifica se a parte é um anexo }
if (parte is TIdAttachment) then begin
anexo := parte as TIdAttachment;
anexo.SaveToFile('c:\temp\' + anexo.FileName);
end;
{ ... }

Mas, como saber quais mensagens podem ser recuperadas ? A quantidade de mensagens existentes é conseguida através da função CheckMessages; podemos recuperar qualquer uma delas usando um número inteiro entre 1 e essa quantidade. A lista é trazida em ordem cronológica, com as mensagens mais antigas da caixa de entrada possuindo os menores números de identificação.
for i := 1 to _IdPOP3.CheckMessages do
begin
msg := TidMessage.Create(self);
_IdPOP3.RetrieveHeader (i, msg);

if DeveRecuperarMsg (msg.MsgId) then
TrataMsg (Msg);
msg.Free;
end;

As mensagens lidas permanecem na caixa até que sejam removidas. O POP3 permite fazer essa remoção mas nem sempre isso é uma opção. Para esses casos, uma sugestão é controlar manualmente as mensagens. Elas possuem uma string de identificação chamada MsgID que é única, como mostra o código acima. A ideia é recuperar apenas o cabeçalho da mensagem, testar se o MsgID atual já foi tratado antes e só então trazer a mensagem completa, se for necessário.

Quando concluir as operações na caixa postal, finalize a conexão com o POP3 usando a função Disconnect.

Como se pode ver pelo exemplo retratado neste post, o protocolo POP3 é bastante simples, limitando-se a poucas funcionalidades básicas. Se for preciso usar recursos mais avançados, tais como restringir as mensagens recuperadas usando filtros ou realizar operações envolvendo "pastas" na caixa postal, teremos que apelar a um protocolo mais completo, como o IMAP4. No post Trabalhando com emails em uma caixa postal usando IMAP4 em Delphi eu mostro como usá-lo.

Mais Informações
Post Office Protocol

45 comentários:

  1. Show de bola! Muito obrigado por compartilhar esta informação!

    ResponderExcluir
  2. cara muito bom , porem to tentando de usar o chekmenssages , mas ele s'o me retorna "0" (zero) mesmo com menssagens na cx de email !

    tem alguma sugestao ?

    ResponderExcluir
  3. Marcelo

    Uma possibilidade é que você tenha feito a conexão com um "servidor / conta de email" diferente daquele que vc imaginou. Revise suas configurações para ver se é esse o caso.

    Veja também se as mensagens que você tem estão na caixa postal ou em subpastas. Não tenho certeza mas acredito que o CheckMessages só é capaz de enxergar o que está diretamente na caixa de entrada.

    Se você precisa trabalhar com pastas ou quer mais flexibilidade no acesso aos emails, use o IMAP4.

    ResponderExcluir
  4. Marcelo :
    muito obrigdo pelo retorno ! fiquei surpreso !
    bom vamos la !
    que esta ocorrendo e que , ao bx a menssagem
    ele nao baixa mais , queria saber se tem como mudar o status da menssagem , para que possa baixa quantas vezez quizer ! sera q tem como ?

    ResponderExcluir
  5. Marcelo

    O que vc quer dizer com "a mensagem não baixa mais" ?. Após serem recuperadas com os métodos mostrados no post, as mensagens continuam disponíveis no servidor - a não ser que sejam removidas.

    O que pode acontecer é ela ser marcada como "já lida". Se quer marcá-la novamente como "não lida", terá que usar o IMAP4 pois o POP3 não prevê essa alternativa.

    ResponderExcluir
  6. Marcelo :
    ok , parti entao para o imap ,
    show de bola!!
    no seu blog imap4 , tem uma procedure ou funcao de nome
    SelecionaCaixaPostal que nao tem seu conteudo no codigo
    teria como vc me fornecer esta ? ou me explicar como funciona !
    muito obrigado !

    ResponderExcluir
  7. Marcelo

    Como diz o comentário incluído no fonte, a função SelecionaCaixaPostal pega os nomes das caixas postais levantadas pelo ListMailBoxes e os mostra num form separado para que o usuário possa escolher qual deve ser aberta.

    A seleção feita pelo usuário é retornada na variável lCaixaSel, que por sua vez é usada em SelectMailBox para efetivamente abrir a caixa escolhida.

    []s

    ResponderExcluir
  8. Ola Luis, Gostaria de mandar para lixeira ou até mesmo apagar as mensagens já recebidas, de uma só vez, isso é possivel usando o POP3, desde já agradeço.

    ResponderExcluir
    Respostas
    1. Souza

      Até onde eu conheço, você terá que usar a função Delete para remover as mensagens. Só que o jeito que ela trabalha exigirá uma chamada para cada mensagem que queira remover, já que ela aceita como parâmetro a identificação de uma única mensagem.

      []s

      Excluir
  9. Muito bom, Luís, estou usando as dicas para construir um mecanismo parecido com o que você descreve no ERP. Abraços!

    ResponderExcluir
  10. eu não ia achar ruim se tivesse ai um "projetinho" pronto pra baixar ^^ rsrs

    se algm tiver um, por favor disponibiliza ae o link, plis!!! =]

    ResponderExcluir
  11. Você tem um "mini-projeto" pronto? deixe-o pra download porque assim fica mais fácil testar do que pegar fragmentos. ^^

    mas quem já está adiantado no Delphi, o POST foi muito bom! Parabéns!

    ResponderExcluir
  12. Muito bom o post!

    voce poderia postar o codigo de "DeveRecuperarMsg" e "TrataMsg"???

    ResponderExcluir
    Respostas
    1. A DeveRecuperarMsg apenas verifica num banco de dados se já foi processada a mensagem com o ID passsado como parâmetro.

      A TrataMsg usa as técnicas descritas no post para recuperar a mensagem e grava no banco de dados.

      []s

      Excluir
  13. Cara valeu , mas meu Count do Message part é sempre 0 (ZERO)!

    como faço para resolver esse problema??

    ResponderExcluir
    Respostas
    1. Guihgo

      Se você chamou o RetrieveHeader, apenas informações de cabeçalho são retornadas (assunto, por exemplo). Se está usando o Retrieve, o MessageParts sejá preenchido automaticamente apenas se a mensagem possuir anexos.

      []s

      Excluir
  14. seria legal se vc fizesse um programa no source forge tipo o outlook feito pelo delphi o q acha? poderiamos aprender muito com ele e ate mesmo contribuir com o mesmo!!!!
    pelas modificaçoes q o indy vem sofrendo acho q seria legal fazer o projeto no xe2 ou quem sabe no q vai ser lançado o xe3 topa?

    ResponderExcluir
    Respostas
    1. Infelizmente, não vou ter tempo de me dedicar a um projeto desses ... Mas, se quiser mesmo, monte-o e submeta as eventuais dúvidas ao blog. Tentarei respondê-las na medida do possível.

      []s

      Excluir
  15. Show de bola! Fiz exatamente como você sugeriu inclusive com a function DeveRecMsg e com a procedure TrataMsg. Perfeito!
    Valeu!

    ResponderExcluir
  16. Primeiro queria parabenizar pelo artigo!!! Fiz todo o procedimento, consegui adequar ao meu sisteminha que estou fazendo, só no remetente quando tem acentos fica tudo zuado, como posso resolver isso?

    ResponderExcluir
    Respostas
    1. Marcos

      Seu servidor de email pode estar retornando as mensagens com codificação UTF-7. A grosso modo, essa codificação mantem intactos os caracteres ASCII de um texto enquanto "expande" os outros 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-. O texto final (Notifica&AOc-&AOM-o) é representado apenas por caracteres ASCII.

      Neste caso, vc pode reverter manualmente a codificação. Neste link do Wikipedia há uma descrição dos algorítmos para codificar e decodificar textos com UTF-7.

      []s

      Excluir
  17. Bom dia,
    Cara, dei uma procurada e não achei, como eu faço para Arquivar uma mensagem com o IdPOP3?

    Abraço.

    ResponderExcluir
    Respostas
    1. Luciano

      O POP3 é um protocolo bastante limitado - basicamente, você pode recuperar uma mensagem e apagá-la do servidor. Para operações mais complexas - como copiar o email para outra pasta (arquivar) - use o IMAP4. Há um post sobre ele neste link.

      []

      Excluir
    2. Boa tarde,
      Pois é, é que neste link sobre IMAP esta postado assim:

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

      Rss, por isso perguntei!

      Abraço.

      Excluir
    3. Achei que vc estivesse se referindo a função Arquivar do Outlook, onde você pode lançar mensagens para um arquivo morto.

      Quando disse que você pode "arquivar" uma mensagem eu me referia a "guardar uma cópia fora do servidor de email". Assim, uma vez que o TidMessage está carregado com a mensagem desejada, vc pode salvá-la em arquivo ou enviá-la para um stream, disponibilizando-a, por exemplo, para um sistema de Base de Conhecimento ou de Gestão de Documentos.

      Obviamente, isso pode ser conseguido tb com o IMAP4. Com esse protocolo, no entanto, vc pode reorganizar os emails em pastas no próprio servidor, sem precisar manter cópias locais.

      []s

      Excluir
    4. Rss, pois é, fiz confusão então.

      Muito obrigado por Responder..

      Fiz um programinha básico, mas vou deixar o link aqui nos comentários, pode ser útil para alguém!

      http://www.4shared.com/rar/OiCMkYt1/IdPOP3.html?
      Está com Senha: idpop3 (minúsculo).

      Abraços

      Excluir
    5. Luciano

      Obrigado por compartilhar sua solução.

      []s

      Excluir
    6. Você tem um exemplo igual este com delphi 2009?

      Excluir
    7. Eu não tenho essa versão do Delphi mas não creio que o Indy tenha mudado a ponto de impedir o código do post de funcionar. Houve algum problema para usar o conteúdo do post com Delphi 2009 ?

      []s

      Excluir
  18. Boa noite,
    Parabéns pelo artigo, muito bom !!!
    Tenho uma pergunta: preciso salvar a mensagem recebida em um arquivo html, tem como fazer ?
    Obrigado.
    Eduardo Flaeschen

    ResponderExcluir
    Respostas
    1. Eduardo

      Se o ContentType do texto do email (TIdText) já indicar que o conteúdo é html, basta salvar esse conteúdo num arquivo HTML.

      Entretanto, se for um texto comum (plain text), você terá que prepará-lo antes de salvar como HTML, montando as tags manualmente.

      []s

      Excluir
  19. Tentei usar no delphi 2009 o demo não funcionou

    ResponderExcluir
    Respostas
    1. Que tipo de problema você enfrentou ? Tenho o Delphi 2005 e o XE, respectivamente com Indy 9 e 10; em ambos a recuperação descrita no post funcionaram normalmente.

      []s

      Excluir
  20. Bom dia, estou utilizando pop3 para baixar email que contem anexo ( XML da NF-e ) porem quando baixo o XML este arquivo vem com falhas dentro do arquivo. Ex: TAG valor NF : 12..90 o correto seria 12,90

    Teria como resolver isso de alguma forma ?

    Obrigado

    ResponderExcluir
    Respostas
    1. Você está fazendo algum outro processamento com o XML? Experimente salvar o conteúdo dele num arquivo para ver se aí já está corrompido.

      Ao extrair anexos, são comuns problemas com acentuação e caracteres especiais. Mas o tipo de problema que vc está descrevendo parece ser de interpretação do conteúdo e o algoritmo para extrair os anexos não faz inferências sobre as tags nesse conteúdo.

      []s

      Excluir
  21. Boa tarde!
    Estou com um problema para implementar, ao incluir somente os campos descritos no artigo, no conectar o programa fica processando e não sai do lugar, usei as mesmas configurações que fiz para o Outlook manualmente(para Hotmail e deu certo), fique com uma dúvida quanto ao "servidor requer ... ssl" no componente Indy não sei como seria o correto configurar isso, tentei meio que adaptar algumas configurações para o pop3 o mesmo que vc usou no outro artigo para SMTP gmail, mas não foi da o seguinte erro "...exception class EidOsslCouldNotLoadSSLLibray whith message 'Could not load SSL library.". Bom é só isso

    ResponderExcluir
    Respostas
    1. Rogério

      Essa mensagem que você está recebendo está relacionada às bibliotecas do projeto OpenSSL utilizadas pelos componentes do Indy para tratar conexões com segurança SSL ou TLS. Você precisa distribuir as duas DLLs desse projeto junto com sua aplicação - libeay32.dll e libssl32.dll.

      Há versões específicas das DLLs para cada versão do Delphi. Então, tem que garantir também que as versões utilizadas são mesmo compatíveis com seu Delphi.

      []s

      Excluir
  22. Opa Ótimo o Post, gostaria de saber se tem como pegar as mensagens por status,como lida ou não, neste método.?

    Grato!

    ResponderExcluir
    Respostas
    1. Felix

      O protocolo POP3 tratado neste post é bastante limitado; o controle de qual mensagem já foi lida tem que ser feito manualmente, como mostrado.

      Se precisar realizar operações mais complexas, use o protocolo IMAP4. Há um post no blog a respeito dele também : Trabalhando com emails em uma caixa postal usando IMAP4 em Delphi.

      []s

      Excluir
  23. Boa tarde. Luís Gustavo, estou com um problema para baixar emails de uma conta, o erro é Socket Error # 10060
    Connection timed out. Tentei resolver aumentando o ConnectTimeout mas não importa o tempo sempre diz a mesma coisa. O que pode ser?

    ResponderExcluir
    Respostas
    1. Cezar

      Esta mensagem está associada à infraestrutura de rede. Pode ser, por exemplo, um bloqueio no firewall; pode ser tb que o servidor de email não esteja acessível a partir do computador onde seu programa está executando. Tente acessar o servidor por outros meios (Ping, hyperterminal, etc) pra certificar que ele esteja disponível.

      []s

      Excluir
    2. Bom dia. Luís Gustavo, configurei o Outlook com os mesmos parâmetros do meu aplicativo e ele consegue baixar sem problema. Já eu consigo apenas enviar. O email que tento acessar é o SquirrelMail. Estou começando a pensar que o problema é ali, pois fiz uma conta gratuita no Bol e então funcionou. Será que tem como resolver isso?

      Excluir
    3. Veja com quem instalou o SquirrelMail se os parâmetros lançados em sua aplicação estão mesmo completos e corretos - o Outlook pode estar usando valores padrão para alguns deles (portas e camadas de segurança, por exemplo). O pessoal de suporte à sua rede tb pode ajudar a determinar a razão para sua conexão remota estar falhando.

      []s

      Excluir

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.