Friday, June 8, 2018

Cliente de Email com JavaFX

Clientes de email são processos que acessam um serviço de email em algum servidor remoto. Esse modelo também é conhecido como modelo cliente-servidor. A essência desse modelo é a existência de um processo cliente e um processo servidor, geralmente em máquinas diferentes, mas nem sempre. No contexto deste post, o processo cliente é uma aplicação Java que acessa os serviços disponibilizados por um servidor de email remoto.

aplicação cliente de email

JavaMail API 

JavaMail API é o framework Java para desenvolver sistemas que enviam, recebem e manipulam mensagens e qualquer tipo de conteudo através de um provedor de emails na internet. Sua especificação é definida pela JSR-919.  O nucleo duro da API fica no pacote javax.mail e é parte integrante do Java Enterprise Edition (No Java Standard Edition é necessário incluir explicitamente a biblioteca para usá-la no seu projeto).

O JavaMail é uma API de alto-nível. Ou seja, ela é capaz de abstrair os componentes de um sistema de email, tais como mensagens, protocolos, pastas, repositório, etc, de maneira bastante intuitiva para o desenvolvedor, isso torna a inclusão de rotinas de mensageria na sua aplicação Java mais fácil e rápida.

Por exemplo, tome-se a classe final javax.mail.Session, que representa uma sessão de comunicação estabelecida entre a aplicação cliente e algum servidor remoto:


Os métodos são auto explicativos:

getInstance(Properties props, Authenticator auth): fabrica uma instância de javax.mail.Session devidamente autenticada com algum servidor de email sob as propriedades definidas no objeto java.util.Properties.

getStore(): retorna o repositório de email daquela conta no servidor de email. A partir de javax.mail.Store você pode acessar as pastas do repositório, como INBOX por exemplo, e manipular as mensagens contidas naquela pasta.

Toda API JavaMail segue esse padrão instuitivo e desacoplado . As principais classes do pacote javax.mail como Store, FolderMessage, Address são abstratas e a implementação concreta fica a cargo do provedor de email com o qual você está trabalhando.
Uma aplicação pode interagir com outra aplicação ou com um ser humano, neste último caso precisamos de uma interface gráfica amigável que possibilite ao usuário conversar com o sistema. JavaFX é a mais recente plataforma multimídia do Java Standard Edition, sucessora do javax.swing, que permite o desenvolvimento de interfaces gráficas avançadas para aplicações desktop e web que executam em uma variedade de dispositivos.

Neste post criamos uma aplicação cliente de email que utiliza:
  1.  API JavaMail para se conectar com o servidor de email remoto 
  2. JavaFX para criar a Interface Gráfica com o Usuário

Projeto Cliente-Servidor

Utilizando o eclipse, navegue pelos menus File/New/project. No assistente que abrir escolha Maven/Maven Project. No próximo assistente marque a opção Create a Simple Project. Next. Dê o nome ao seu projeto de JavaFxEmailClient e finalize.

O arquivo de coordenadas pom.xml fica como segue:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>JavaFxEmailClient</groupId>
 <artifactId>JavaFxEmailClient</artifactId>
 <version>2.0</version>
 <properties>
  <maven.compiler.source>1.8</maven.compiler.source>
  <maven.compiler.target>1.8</maven.compiler.target>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>
 <dependencies>
  <dependency>
   <groupId>javax</groupId>
   <artifactId>javaee-api</artifactId>
   <version>8.0</version>
  </dependency>  
 </dependencies>
</project>


Agora clique com o botão direito no diretório raiz de seu projeto e escolha MAVEN/Update Project. Pronto. Agora que nosso projeto está devidamente configurado podemos começar a criar as classes. O seguinte diagrama de classes define o design do nosso cliente de email:



ArquivoModel.java
Como nosso sistema cliente poderá enviar mensagens com anexos, a classe ArquivoModel representa um Wrapper do arquivo real. O padrão Wrapper serve para encapsular funcionalidades e criar um nível adicional de abstração:
package main;

//Classe Wrapper para trabalhar com um arquivo anexado 
public class ArquivoModel {

 private String nomeArquivo;
 private byte[] conteudo;
 private String mimeType;
 
 public ArquivoModel(String nomeArquivo, byte[] conteudo, String mimeType) {
  super();
  this.nomeArquivo = nomeArquivo;
  this.conteudo = conteudo;
  this.mimeType = mimeType;
 }
   //getters & seters omitidos...
}


JanelaEnviar.java
A classe JanelaEnviar extende o container javafx.scene.layout.GridPane. Ela agrega todos os controles visuais que fazem a interface gráfica com o usuário e captura os eventos disparados pelo usuário:
package main;
//IMPORTS OMITIDOS...
//interface gráfica com o usuário GUI
public class JanelaEnviar extends GridPane {

 //controles da GUI
 TextField txtHostServer;
 TextField txtPara;
 TextField txtDe;
 TextField txtAssunto;
 TextField txtUsername;
 PasswordField txtSenha;
 Button btoEnviar;
 Label lblHostServer;
 Label lblPara;
 Label lblDe;
 Label lblAssunto;
 Label lblUserName;
 Label lblSenha;
 Text mensagem;

 // tabela de arquivos anexados
 TableView<ArquivoModel> tabelaAnexos;

 // lista de arquivos exibidos pela tabela
 ObservableList<ArquivoModel> listAnexos;

 // widget do javafx que gera tags html
 HTMLEditor htmlEditor;

 Scene scene;

 public JanelaEnviar(EventHandler<ActionEvent> eventControler) {
  // TODO Auto-generated constructor stub

  this.setAlignment(Pos.CENTER);
  this.setVgap(5);
  this.setHgap(5);
  this.setPadding(new Insets(10));

  txtHostServer = new TextField("smtp.gmail.com");
  txtPara = new TextField();
  txtDe = new TextField();
  txtAssunto = new TextField();
  txtUsername = new TextField();
  txtSenha = new PasswordField();

  btoEnviar = new Button("Enviar");
  btoEnviar.setOnAction(eventControler);

  lblHostServer = new Label("SMTP Server:");
  lblPara = new Label("Para:");
  lblDe = new Label("De:");
  lblAssunto = new Label("Assunto:");
  lblUserName = new Label("Login:");
  lblSenha = new Label("Senha:");
  mensagem = new Text();
  htmlEditor = new HTMLEditor();
  htmlEditor.setPrefHeight(430);

  // node, col, row
  this.add(lblHostServer, 0, 0);
  this.add(txtHostServer, 1, 0);
  this.add(lblPara, 0, 1);
  this.add(txtPara, 1, 1);
  this.add(lblDe, 0, 2);
  this.add(txtDe, 1, 2);
  this.add(lblAssunto, 0, 3);
  this.add(txtAssunto, 1, 3);
  this.add(lblUserName, 0, 4);
  this.add(txtUsername, 1, 4);
  this.add(lblSenha, 0, 5);
  this.add(txtSenha, 1, 5);

  this.add(setUpTabelaAnexos(), 0, 6, 2, 1);

  this.add(htmlEditor, 0, 7, 2, 1);

  VBox vBox = new VBox(5, btoEnviar, mensagem);
  vBox.setAlignment(Pos.CENTER);
  this.add(vBox, 0, 8, 2, 1);

  scene = new Scene(this);
 }

 // método auxiliar de configuraçã da tabela
 private Node setUpTabelaAnexos() {

  listAnexos = FXCollections.observableArrayList();

  // vincula a list na tabela
  tabelaAnexos = new TableView<>(listAnexos);

  // mensagem quando a tabela estiver vazia
  tabelaAnexos.setPlaceholder(new Label("Nenhum Arquivo Anexado"));

  // a tabela tera 2 colunas: nome do arquivo e outra coluna com um botão em cada
  // linha para excluir o anexo,
  // caso o usuário mude de ideia

  // configurando a coluna nome do arquivo
  TableColumn<ArquivoModel, String> colNomeArquivo = new TableColumn<>("Tabela de Anexos");

  // especifica o tipo de dado da coluna e em qual campo do objeto ele está
  colNomeArquivo.setCellValueFactory(new PropertyValueFactory<>("nomeArquivo"));
  colNomeArquivo.setPrefWidth(450);

  // coluna utilitária que possui um botão remover em cada linha
  TableColumn<ArquivoModel, ArquivoModel> colRemoveAnexo = new TableColumn<>();
  colRemoveAnexo.setPrefWidth(80);
  colRemoveAnexo.setCellValueFactory(param -> new ReadOnlyObjectWrapper<>(param.getValue()));

  // configura o conteudo da coluna com um objeto TableCell personalizado
  colRemoveAnexo.setCellFactory(coluna -> new TableCell<ArquivoModel, ArquivoModel>() {

   final Button btoRemoveAnexo = new Button("X");

   // metodo updateItem é chamado automaticamente quando se constroi a tabela
   @Override
   protected void updateItem(ArquivoModel arquivoAnexo, boolean empty) {

    super.updateItem(arquivoAnexo, empty);

    // se a linha for vazia, não faz nada
    if (arquivoAnexo == null) {
     setGraphic(null);
     return;
    }
    // se tiver registro na linha, configura o botão
    setGraphic(btoRemoveAnexo);
    // toda vez quando o botão é clicado, o respectivo anexo é removido
    btoRemoveAnexo.setOnAction(event -> getTableView().getItems().remove(arquivoAnexo));
   };
  });

  // adiciona as coluna na tabela
  tabelaAnexos.getColumns().addAll(colNomeArquivo, colRemoveAnexo);
  tabelaAnexos.setPrefHeight(150);

  // configurando o botão anexar
  Button btoAddAnexo = new Button("Anexar...");

  // registrando o evento de ação para quando o botão for acionado pelo usuário
  btoAddAnexo.setOnAction(event -> {
   try {
    // dialog para seleção de arquivos
    FileChooser fileChooser = new FileChooser();
    fileChooser.setTitle("Escolher Anexo");

    // exibe a dialog de seleção de arquivos vinculada à JanelaEnviar
    // quando o usuário fecha a dialog de seleção ela retorna um objeto java.io.File
    Path anexo = fileChooser.showOpenDialog(((Node) event.getSource()).getScene().getWindow()).toPath();

    // verifica se o anexo realmente existe
    if (Files.exists(anexo)) {

     // extrai o tipo de dado que o anexo contem
     String mimeType = Files.probeContentType(anexo);
     System.out.printf("mime type %s", mimeType);

     // converte o anexo em byte[]
     byte[] conteudo = Files.readAllBytes(anexo);

     // extrai o nome do anexo
     String nomeAnexo = anexo.getFileName().toString();

     // cria o objeto ArquivoModel, utilitario para manipular os anexos
     ArquivoModel novoAnexo = new ArquivoModel(nomeAnexo, conteudo, mimeType);

     // adiciona o anexo na lista vinculada à tabela
     listAnexos.add(novoAnexo);
    }
   } catch (NullPointerException | IOException e) {
    e.printStackTrace();
   }
  });

  // colaca a tabela e o botão addAnexo em uma VBox e retorna
  VBox vBoxTabelaAnexo = new VBox(5, tabelaAnexos, btoAddAnexo);

  return vBoxTabelaAnexo;
 }

 // retorna os anexos
 ArquivoModel[] getAnexos() {
  return listAnexos.toArray(new ArquivoModel[listAnexos.size()]);
 }

 void setMensagemDeSucesso(String msg) {
  mensagem.setFill(Color.GREEN);
  mensagem.setText(msg);
 }

 void setMensagemDeErro(String msg) {
  mensagem.setFill(Color.RED);
  mensagem.setText(msg);
 }
}

EventoEnviarEmail.java
A classe EventoEnviarEmail gerencia os eventos do usuário que ocorram na classe JanelaEnviarEmail. Os objetos da API JavaMail são usados aqui:
package main;

//IMPORTS OMITIDOS
//Controle de Eventos da JanelaEnviar
public class EventoEnviarEmail implements EventHandler{

 JanelaEnviar janelaEnviar;
 String hostServer;
 String para;
 String de;
 String assunto;
 String username; 
 String senha;
 String conteudoHtmlMensagem;
 
 ArquivoModel[] anexos;   
 
 public void handle(ActionEvent event) {
  // TODO Auto-generated method stub
  preencherCampos();
  
  try {      
   enviarEmail();          
   janelaEnviar.setMensagemDeSucesso("Email Enviado com Sucesso");
   System.out.println("OK");
  }
  catch (UnsupportedEncodingException e) {
   janelaEnviar.setMensagemDeErro("Caracteres não suportados na mensagem");
   e.printStackTrace();
  }
  catch (AuthenticationFailedException e) {
   //endereco invalido
   janelaEnviar.setMensagemDeErro("Usuário ou senha Inválidos");
   e.printStackTrace();
  }
  catch (AddressException e) {
   //endereco invalido
   janelaEnviar.setMensagemDeErro("Formato de endereço de email Inválido");
   e.printStackTrace();
  }
  catch (MessagingException e) {
   //endereco invalido
   janelaEnviar.setMensagemDeErro("Erro ao enviar a mensagem");
   e.printStackTrace();
  }
  catch (Exception e) {
   // TODO: handle exception
   janelaEnviar.setMensagemDeErro("Erro ao enviar a mensagem");
   e.printStackTrace();
  }
  finally {
   janelaEnviar.txtSenha.setText("");
  }
 }
 
 private void enviarEmail() throws AddressException, MessagingException, UnsupportedEncodingException {
  
  // TODO Auto-generated method stub
  Authenticator authenticator = new MeuAutenticador(username, senha);
  
  //configura objeto properties
  Properties props = new Properties();
  props.put("mail.smtp.host", hostServer);  
  props.put("mail.smtp.host", "smtp.gmail.com");
  props.put("mail.smtp.socketFactory.port", "465"); //porta ssl
  props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); //especifica classe para criar SMTP Socket
  props.put("mail.smtp.auth", "true"); //autenticação requerida  
  
  //obtem um objeto Session sob as credenciais e propriedades especificadas
  Session session = Session.getInstance(props, authenticator);
  
  Message mensagem = new MimeMessage(session);
  
  Address enderecoDe = new InternetAddress(username, de); //address except
  Address enderecoPara = new InternetAddress(para);
  
  mensagem.setFrom(enderecoDe); //messaging excep
  mensagem.setRecipient(Message.RecipientType.TO, enderecoPara);
  mensagem.setSubject(assunto);  
  
  //coloca o conteudo e os anexos na mensagem
  setConteudoMensagem(mensagem, anexos);    
    
  Transport.send(mensagem);  //auth failed except
 }
 
 private void setConteudoMensagem(Message mensagem, ArquivoModel[] arquivos) throws MessagingException {
  
  Multipart multipart = new MimeMultipart();  
  
  //cria um BodyPart representando um conteudo HTML
  BodyPart messageBodyPart = new MimeBodyPart();    
  
  messageBodyPart.setContent(conteudoHtmlMensagem, "text/html");
  
  //adiciona o BodyPart nop Multipart
  multipart.addBodyPart(messageBodyPart);
  
  messageBodyPart = new MimeBodyPart();
  
  //DataHandler para operações diversas nos diferentes tipos de dados
  DataHandler dataHandler = null;     
       
  //loop: cria um BodyPart para cada anexo e adiciona-o no Multipart 
  for(ArquivoModel anexo : arquivos) {
   
   dataHandler = new DataHandler(anexo.getConteudo(), anexo.getMimeType());
   messageBodyPart = new MimeBodyPart();         
   messageBodyPart.setDataHandler(dataHandler);
   messageBodyPart.setFileName(anexo.getNomeArquivo());
   multipart.addBodyPart(messageBodyPart);
  }  
  
  //adiciona o MUltipart na mensagem
  mensagem.setContent(multipart);
 }
  

 private void preencherCampos() {
  // TODO Auto-generated method stub
  hostServer = janelaEnviar.txtHostServer.getText();
  para = janelaEnviar.txtPara.getText();
  de = janelaEnviar.txtDe.getText();
  assunto = janelaEnviar.txtAssunto.getText();
  username = janelaEnviar.txtUsername.getText();
  senha = janelaEnviar.txtSenha.getText();
  anexos = janelaEnviar.getAnexos();
  //extrai o conteudo html de HTMLEditor na forma de uma String 
  conteudoHtmlMensagem = janelaEnviar.htmlEditor.getHtmlText();  
 } 
}

MeuAutenticator.java
No JavaMail, para logar em uma conta de email no servidor remoto, você precisa sobrescrever o método getPasswordAuthentication() da classe javax.mail.Authenticator porque é esse método que o objeto javax.mail.Session chama para efetivar o login. Se a tentativa de login feita por Session falhar, a exceção javax.mail.AuthenticationFailedException é lançada:
package main;

//IMPORTS OMITIDOS
public class MeuAutenticador extends Authenticator {

 private final PasswordAuthentication passwordAuthentication; 
 
 //passamos login e senha no construtor
 public MeuAutenticador(String login, String senha) {
  super();
  passwordAuthentication = new PasswordAuthentication(login, senha);
 }
 
 //toda aplicação deve sobrescrever getPasswordAuthentication()
 @Override
 protected PasswordAuthentication getPasswordAuthentication() {
  // TODO Auto-generated method stub
  
  return passwordAuthentication;
 }
}

Main.java
Na classe Main iniciamos a aplicação. Para inciar uma aplicação JavaFx, sobrescrevemos o método start() da classe abstrata javafx.application.Application:
package main;

import javafx.application.Application;
import javafx.stage.Stage;

//Enviando conteudo HTML
public class Main extends Application {

 @Override
 public void start(Stage primaryStage) throws Exception {
  // TODO Auto-generated method stub
  EventoEnviarEmail evento = new EventoEnviarEmail();
  JanelaEnviar janelaEnviar = new JanelaEnviar(evento);
  
  evento.janelaEnviar = janelaEnviar;
  
  primaryStage.setTitle("Cliente de Email com JavaFX");
  primaryStage.setScene(janelaEnviar.scene);  
  primaryStage.show();  
 }

 public static void main(String[] args) {
  // TODO Auto-generated method stub
  launch(args);
 }
}

Agora já podemos executar o projeto. O resultado é a aplicação que exibimos no início deste post:



Após preencher todos os campos, envie a mensagem ao destinatário clicando no botão enviar.


       

No comments:

Post a Comment