Nesta primeira parte, abordarei o cliente, ou seja, o programa irá conectar em algum servidor. Tive a idéia de fazer um código para enviar e-mails, já que o protocolo SMTP é simples e fácil para mensagens em texto plano e sem anexos. Para quem não conhece o protocolo SMTP, sugiro que leia a RFC822, que descreve seu formato:
ftp://ftp.rfc-editor.org/in-notes/rfc822.txt
De qualquer forma, vou dar uma resumida no protocolo.
Nos parágrafos abaixo, <- indica uma mensagem que está vindo do servidor e -> indica uma mensagem que o cliente está enviando para o servidor.
Ao se conectar em um servidor SMTP, o cliente recebe uma mensagem de boas-vindas, geralmente com o endereço do servidor e alguns dados do software:
<- 220 spaceymail-mx1.g.dreamhost.com ESMTP
Em seguida, o cliente deve se idenficar enviando a string "EHLO" e o seu endereço. Após a identificação, o servidor dá algumas informações sobre o envio da mensagem (não relevante para nós no momento):
-> EHLO just.justsoft.com.br
<- 250-spaceymail-mx1.g.dreamhost.com
<- 250-PIPELINING
<- 250-SIZE 40960000
<- 250-ETRN
<- 250-STARTTLS
<- 250 8BITMIME
Agora o cliente deve dizer o endereço do remetente com o comando "MAIL From:" e do destinatário com "RCPT To:":
-> MAIL From: <teste@teste.com>
<- 250 Ok
-> RCPT To: <jpjust@justsoft.com.br>
<- 250 Ok
Após estas informações, o cliente começa a mensagem com o comando "DATA":
-> DATA
<- 354 End data with <CR><LF>.<CR><LF>
-> From: <just@just.com>
-> To: <jpjust@justsoft.com.br>
-> Date, Thu, 15 Mar 2007 21:00:00 +0000
-> Subject: Teste
->
-> teste
->
-> .
<- 250 Ok: queued as 6C0C3CE937
O fim da mensagem deve ser indicado com um ponto (.) sozinho em uma linha. Por último, o cliente pede para o servidor fechar a conexão com "QUIT":
-> QUIT
<- 221 Bye
E é isto! Você pode testar os comandos conectando em algum servidor SMTP pelo telnet. Basta executar o comando "telnet mx1.hotmail.com 25" para conectar no servidor SMTP do Hotmail e enviar mensagens para endereços de lá. Veja abaixo uma pequena lista com o servidor SMTP de alguns serviços de e-mail famosos:
Hotmail: mx1.hotmail.com
Yahoo!: a.mx.mail.yahoo.com
GMail: gmail-smtp-in.l.google.com
UOL: mx.uol.com.br
BOL: mx.boil.com.br
Se você usa Linux ou algum outro UNIX, use o comando host para saber o servidor SMTP de algum domínio de e-mail. Por exemplo, para saber o servidor SMTP de gmail.com:
$ host -t MX gmail.com
Voltando à programação, o que iremos fazer é um programa em wxWidgets para se conectar a um servidor SMTP e enviar um e-mail.
A parte wxWidgets da coisa
Para todo o trabalho de rede no wxWidgets, usaremos a classe wxSocketBase e uma classe derivada, a wxSocketClient.
wxSocketBase é a classe base para todas as outras classes de socket no wxWidgets. Neste post, usaremos apenas a classe wxSocketClient, que é responsável por fazer as conexões do nosso programa, que é o cliente, no servidor.
Conectando-se no servidor
Antes de fazer alguma conexão com a wxSocketClient, precisamos do endereço do servidor ao qual vamos nos conectar e também a porta da conexão. Para manusear essas informações, usaremos a classe wxIPV4address. Essa classe serve para guardar informações de um endereço, como número IP e porta. Para definir o endereço do servidor e a porta, usamos dois métodos:
wxIPV4address host;
host.Hostname(wxT("smtp.server.com"));
host.Service(wxT("25"));
No trecho de código acima, criamos um objeto de nome host, que é uma instância de wxIPV4address. Em seguida, definimos o endereço como smtp.server.com e a porta como 25.
Após criar um objeto com o endereço e a porta do servidor, podemos chamar o método wxSocketClient::Connect() para conectar no servidor:
wxSocketClient sock;
bool status;
status = sock.Connect(host);
Se a conexão for feita com sucesso, o método retorna true, que será guardado na variável status. Em caso de erro na conexão, logicamente, o método retornará false.
Por padrão, o método wxSocketClient::Connect() aguarda a conexão ser feita ou a ocorrência de um erro para prosseguir a execução. Mas é possível chamar o método e continuar a execução do programa enquanto o socket está se conectando, basta adicionar um parâmetro ao método:
sock.Connect(host, false);
O segundo parâmetro indica se o método deverá aguardar a conexão ser completada. Se você escolheu esta maneira para se conectar, poderá verificar se a conexão foi feita posteriormente com o método wxSocketBase::IsConnected() ou até mesmo, aguardar pela conexão em um ponto posterior com wxSocketClient::WaitOnConnect().
Enviando dados
Após ter sido feita a conexão, já é possível enviar e receber dados. Para fazer o envio, usamos o método wxSocketBase::Write(). Após enviar qualquer dado, podemos verificar se o envio foi feito com sucesso com o método wxSocketBase::Error():
wxString dado = wxT("EHLO localhost\r\n");
socket.Write((char *)dado.mb_str(), dado.Len());
if (socket.Error())
{
wxMessageBox(wxT("Erro ao enviar o dado."));
}
No trecho acima, enviamos a string "EHLO localhost\r\n" e em caso de erro, uma mensagem é exibida ao usuário. Perceba o cast (char *) e o método wxString::mb_str(). Este método retorna a string no formato ANSI. Perceba também que o método wxSocketBase::Write() não define um tipo de dado específico, você pode enviar texto puro ou binário. Como no nosso exemplo de um cliente SMTP estamos enviando texto puro, obtemos o formato ANSI da string e usamos o cast (char *) para indicar ao método que estamos enviando um char. O segundo parâmetro do método indica o tamanho do dado que estamos enviando. No caso, como é texto puro em ANSI (1 byte por caracter), indicamos o tamanho da string.
Em caso de erro no envio, wxSocketBase::Error() retorna true.
Recebendo dados
Após enviar algum dado pelo socket, geralmente esperamos por uma resposta, esta é a hora de fazer a leitura do socket. Para isto, usaremos o método wxSocketBase::Read():
char buffer[1024] = {0};
socket.Read(buffer, 1024 - 1);
if (socket.Error())
{
wxMessageBox(wxT("Erro ao fazer a leitura."));
}
int contagem = socket.LastCount();
Primeiro, criamos um buffer do tipo char com 1 KB. Em seguida, fazemos a leitura, armazenando a saída em buffer e indicando o máximo de dados que deverá ser lido (subtraímos 1 do tamanho pois devemos guardar um espaço para o terminador de string \0 da variável).
Mais uma vez, wxSocketBase::Error() entra em ação para nos informar se houve algum erro.
Neste trecho, usamos também o método wxSocketBase::LastCount(), ele retorna o número de bytes lidos no wxSocketBase::Read(). Ele também pode ser usado após um wxSocketBase::Write() para saber quantos bytes foram enviados de fato.
Fechando a conexão
Após enviarmos e recebermos todos os dados necessários para a conexão, devemos terminá-la. Basta um único método para isso, o wxSocketBase::Close():
socket.Close()
Existe também um outro método relacionado ao término de conexão, o wxSocketBase::WaitForLost(). Com ele, você pode indicar um timeout em segundos ou milissegundos. Se a conexão for fechada antes do timeout (o servidor pode fechar a conexão), o método retorna true, caso o timeout seja atingido, ele retorna false.
Outras formas de E/S
Além do wxSocketBase::Read() e do wxSocketBase::Write(), existem também o wxSocketBase::WriteMsg() e o wxSocketBase::ReadMsg(). Com esses dois métodos, é possível trocar mensagens entre duas aplicações em wxWidgets sem precisar se preocupar com a contagem de bytes enviados ou recebidos.
Mostrarei estes e outros métodos das classes de socket em posts futuros. Ainda há muito o que falar sobre sockets no wxWidgets :)
Configurando o socket
Também é possível fazer algumas configurações no socket antes de utilizá-lo. Veja abaixo.
wxSocketBase::SetTimeout(): configura o tempo de timeout em segundos para os métodos de E/S e de espera do socket. O valor padrão é de 10 minutos.
socket.SetTimeout(120); // Configura o timeout para 120 segundos
wxSocketBase::SetFlags(): configura o comportamento de espera do socket nas operações de E/S. Pode receber como argumento os valores abaixo:
wxSOCKET_NONE: Funcionamento normal.
wxSOCKET_NOWAIT: Lê ou grava o máximo possível de dados e retorna imediatamente.
wxSOCKET_WAITALL: Aguarda que todo o dado seja lido ou gravado ou que um erro ocorra para retornar.
wxSOCKET_BLOCK: Bloqueia a interface gráfica durante a operação.
wxSOCKET_REUSEADDR: Permite que o socket escute em uma porta que já está em uso (apenas para sockets de servidor).
Para configurar o socket para sempre aguardar que os dados sejam lidos ou gravados por completo antes de continuar a execução do código, use:
socket.SetFlags(wxSOCKET_WAITALL);
Outros métodos de configuração do socket serão vistos nos outros posts.
Nosso código de exemplo
Finalmente, o código do programa que irá fazer a conexão e enviar o e-mail. O código está bem documentado para que você possa entender cada parte do programa.
A interface gráfica já vai aparecer preenchida com os dados do servidor SMTP do GMail e meu endereço de e-mail de lá. Por favor, envie um e-mail para mim pelo programa de exemplo :) Obrigado.
/* A Casa de Just - http://jpjust.blogspot.com
* Curso de wxWidgets: Enviando e-mails via SMTP
*
* O objetivo deste código-fonte é demonstrar diversas classes
* ensinadas no curso de wxWidgets do blog "A Casa de Just".
*
* As aulas do curso de wxWidgets podem ser encontradas em forma
* de posts no blog: http://jpjust.blogspot.com
*
* Copyright (c) João Paulo Just <jpjust@justsoft.com.br>
* A Casa de Just - http://jpjust.blogspot.com
* 18 de março de 2007, 00:48, Ilhéus, BA, Brasil.
*/
#include <wx/wx.h>
#include <wx/socket.h>
#include <string.h>
#include <time.h>
// Tamanho do buffer que será utilizado no recebimento de mensagens
#define BUFFER 256
// Enumeração dos IDs
enum
{
ID_ENVIAR
};
// Classe: MailApp
class MailApp: public wxApp
{
public:
virtual bool OnInit();
};
// Classe: MailFrame
class MailFrame: public wxFrame
{
public:
MailFrame(void);
DECLARE_EVENT_TABLE()
private:
void EnviarMensagem(wxCommandEvent &event); // Método para enviar a mensagem
wxString Envia(wxSocketBase *socket, wxString msg); // Método para enviar dados pelo socket
wxString Le(wxSocketBase *socket); // Método para obter dados no socket
wxStaticText *lb_servidor;
wxStaticText *lb_porta;
wxStaticText *lb_de;
wxStaticText *lb_para;
wxStaticText *lb_assunto;
wxTextCtrl *txt_servidor;
wxTextCtrl *txt_porta;
wxTextCtrl *txt_de;
wxTextCtrl *txt_para;
wxTextCtrl *txt_assunto;
wxTextCtrl *txt_mensagem;
wxTextCtrl *txt_proto;
wxButton *btn_enviar;
};
// Tabela de eventos
BEGIN_EVENT_TABLE(MailFrame, wxFrame)
EVT_BUTTON(ID_ENVIAR, MailFrame::EnviarMensagem)
END_EVENT_TABLE()
// Método: MailApp::OnInit()
// Inicialização do programa
bool MailApp::OnInit()
{
MailFrame *frame = new MailFrame();
frame->Show();
return true;
}
// Método: MailFrame::MailFrame
// Construtor do frame
MailFrame::MailFrame(void)
:wxFrame(NULL, wxID_ANY, wxT("Enviar e-mail - http://jpjust.blogspot.com"))
{
//wxMessageBox(wxNow());
// Sizers
wxGridSizer *sizer_g = new wxGridSizer(5, 2, 0, 0);
wxBoxSizer *sizer_v = new wxBoxSizer(wxVERTICAL);
// Texto indicativo
lb_servidor = new wxStaticText(this, wxID_ANY, wxT("Servidor SMTP:"));
lb_porta = new wxStaticText(this, wxID_ANY, wxT("Porta:"));
lb_de = new wxStaticText(this, wxID_ANY, wxT("De:"));
lb_para = new wxStaticText(this, wxID_ANY, wxT("Para:"));
lb_assunto = new wxStaticText(this, wxID_ANY, wxT("Assunto:"));
// Caixas de texto
txt_servidor = new wxTextCtrl(this, wxID_ANY, wxT("gmail-smtp-in.l.google.com"), wxDefaultPosition, wxSize(200, -1));
txt_porta = new wxTextCtrl(this, wxID_ANY, wxT("25"));
txt_de = new wxTextCtrl(this, wxID_ANY, wxT("seu@email.com"), wxDefaultPosition, wxSize(200, -1));
txt_para = new wxTextCtrl(this, wxID_ANY, wxT("just1982@gmail.com"), wxDefaultPosition, wxSize(200, -1));
txt_assunto = new wxTextCtrl(this, wxID_ANY, wxT("Post 14"), wxDefaultPosition, wxSize(200, -1));
txt_mensagem = new wxTextCtrl(this, wxID_ANY, wxT("Eu li o post 14!"), wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_WORDWRAP);
txt_proto = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_WORDWRAP | wxTE_READONLY);
// Botão de enviar
btn_enviar = new wxButton(this, ID_ENVIAR, wxT("Enviar"));
// Adiciona itens nos sizers
sizer_g->Add(lb_servidor, 0, wxALL, 5);
sizer_g->Add(txt_servidor, 0, wxALL, 5);
sizer_g->Add(lb_porta, 0, wxALL, 5);
sizer_g->Add(txt_porta, 0, wxALL, 5);
sizer_g->Add(lb_de, 0, wxALL, 5);
sizer_g->Add(txt_de, 0, wxALL, 5);
sizer_g->Add(lb_para, 0, wxALL, 5);
sizer_g->Add(txt_para, 0, wxALL, 5);
sizer_g->Add(lb_assunto, 0, wxALL, 5);
sizer_g->Add(txt_assunto, 0, wxALL, 5);
sizer_v->Add(sizer_g, 0, wxALL, 0);
sizer_v->Add(txt_mensagem, 2, wxALL | wxEXPAND, 5);
sizer_v->Add(txt_proto, 1, wxALL | wxEXPAND, 5);
sizer_v->Add(btn_enviar, 0, wxALL | wxALIGN_RIGHT, 5);
SetSizerAndFit(sizer_v);
}
/////////////////////////////////////////////////
// A partir daqui, a interface gráfica já está criada e
// veremos os métodos que realmente importam neste exemplo.
// Método: Envia
// Envia 'msg' para o socket e efetua a leitura logo em seguida, retornando o resultado
wxString MailFrame::Envia(wxSocketBase *socket, wxString msg)
{
// Se estiver desconectado, sai do método e retorna uma string vazia
if (socket->IsDisconnected())
return wxEmptyString;
wxString res;
// Envia 'msg' pelo socket
socket->Write((char *)msg.mb_str(), msg.Len());
// Em caso de erro, retorna uma mensagem avisando e fecha a conexão
if (socket->Error())
{
return wxT(">> Ocorreu um erro ao se comunicar com o servidor!\n");
socket->Close();
}
// Recebe a resposta enviada pelo outro host e a retorna
return msg + Le(socket);
}
// Método: Le
// Lê o conteúdo do socket (ou seja, qualquer mensagem enviada pelo outro host)
wxString MailFrame::Le(wxSocketBase *socket)
{
wxString res;
char buf[BUFFER]; // Buffer para recebimento
do
{
memset(buf, 0, BUFFER);
socket->Read(buf, BUFFER - 1); // Faz a leitura e armazena no buffer
res.Append(buf);
// Enquanto 'res' estiver vazio (nenhuma leitura foi feita ainda) ou
// enquanto houver dados para serem lidos, continuaremos percorrendo o laço
} while ((socket->LastCount() > 0) || (res.Len() == 0));
return res;
}
// Método: MailFrame::Envia
// Envia o e-mail
// Este método vai fazer a conexão, enviar os dados e fechar a conexão
void MailFrame::EnviarMensagem(wxCommandEvent &event)
{
wxIPV4address host;
wxSocketClient sock;
wxString msg, saida;
// Obtém a hora no formato requerido pela RFC822
// O formato é "Dia, data mês ano hora fuso"
// Ex.: Thu, 15 Mar 2007 20:19:00 BRT
char hora[50] = {0};
time_t now = time(NULL);
strftime(hora, 50, "%a, %d %b %Y %T %Z", localtime(&now));
// Configura o objeto 'host'
// Aqui definimos o endereço do servidor e a porta
host.Hostname(txt_servidor->GetValue());
host.Service(txt_porta->GetValue());
// Configuração do socket
// O timeout padrão para operações de E/S será de 120 segundos
// A flag 'wxSOCKET_NOWAIT' indica que operações de E/S irão retornar imediatamente
// (com esta flag, o programa não irá ficar parado em um Read() ou Write() do socket)
sock.SetTimeout(120);
sock.SetFlags(wxSOCKET_NOWAIT);
txt_proto->AppendText(wxT(">> Tentando se conectar...\n"));
if (sock.Connect(host) == false)
{
// Erro na conexão
txt_proto->AppendText(wxT(">> Ocorreu um erro ao tentar conectar no servidor!\n"));
return;
}
txt_proto->AppendText(wxT(">> Conectado!\n"));
txt_proto->AppendText(Le(&sock)); // Lê a mensagem de boas-vindas do servidor
// Neste bloco, enviamos uma identificação (EHLO), o remetente (MAIL From),
// o destinatário (RCPT To) e indicamos o início da mensagem (DATA)
txt_proto->AppendText(Envia(&sock, wxT("EHLO ") + wxGetFullHostName() + wxT("\r\n")));
txt_proto->AppendText(Envia(&sock, wxT("MAIL From: <") + txt_de->GetValue() + wxT(">\r\n")));
txt_proto->AppendText(Envia(&sock, wxT("RCPT To: <") + txt_para->GetValue() + wxT(">\r\n")));
txt_proto->AppendText(Envia(&sock, wxT("DATA\r\n")));
// Agora, o e-mail será montado. O corpo do e-mail tem o seguinte formato:
//
// From: "Nome do remetente" <email_do_remetente>
// To: "Nome do destinatário" <email_do_destinatário>
// Date: Data de envio (obedecendo a RFC822)
// Subject: Assunto do e-mail
//
// Mensagem, linha 1...
// Mensagem, linha 2...
//
// . (deve conter um ponto na última linha para indicar o fim da mensagem)
msg.Clear();
msg.Append(wxT("From: <") + txt_de->GetValue() + wxT(">\r\n"));
msg.Append(wxT("To: <") + txt_para->GetValue() + wxT(">\r\n"));
msg.Append(wxT("Date: ") + wxString(hora) + wxT("\r\n"));
msg.Append(wxT("Subject: ") + txt_assunto->GetValue() + wxT("\r\n\r\n"));
msg.Append(txt_mensagem->GetValue() + wxT("\r\n\r\n"));
msg.Append(wxT("--\r\nVisite A Casa de Just: http://jpjust.blogspot.com\r\n"));
msg.Append(wxT("\r\n.\r\n"));
// O corpo do e-mail é enviado pelo socket e a resposta do servidor é
// inserida na caixa de texto
txt_proto->AppendText(Envia(&sock, msg));
// Por último, fechamos a conexão com o comando QUIT
txt_proto->AppendText(Envia(&sock, wxT("QUIT\r\n")));
// Aguarda que a conexão seja fechada pelo servidor
if (sock.WaitForLost(120) == false)
{
txt_proto->AppendText(wxT(">> Erro ao enviar mensagem!\n"));
}
else
{
txt_proto->AppendText(wxT(">> Mensagem enviada!\n"));
}
// Pronto! O e-mail está enviado! :)
}
IMPLEMENT_APP(MailApp)