O czym będzie ten artykuł?
Mimo istnienia programów pocztowych bywa, że chcey wysłać e-maila z poziomu naszego programu. Da się to zrobić w C++, m.in. przy użyciu biblioteki POCO. Niniejszy artykuł stanowi pewnego rodzaju kontynuację artykułu o pobieraniu maili przy użyciu biblioteki POCO przy użyciu protokołu POP3
[C++, POCO] Pobieranie poczty przy użyciu protokołu POP3 za pomocą biblioteki POCO, dlatego pominę kwestie instalacji biblioteki.
Wysyłanie poczty
Prawie wszyscy dostawcy poczty internetowej dostarczają serwery SMTP do wysyłania wiadomości, mimo iż ten protokół jest dość stary i ubogi w funkcjonalności. Ja akurat testowałem ten artykuł dla Poczt O2, WP i AGH, dla których adresy serwera poczty przychodzącej POP3 to odpowiednio:
poczta.o2.pl,
smtp.wp.pl i
poczta.agh.edu.pl. Jeśli, Drogi Czytelniku, masz konto na innym serwerze, nie martw się, bo dla każdego porządnego serwera pocztowego adresy serwerów poczty są łatwo dostępne w internecie.
Niektóre serwisy poczty internetowej wymagają włączenia możliwości wysyłania poczty przez SMTP. |
Wysyłanie poczty z terminala
Mimo iż artykuł jest o wysyłaniu poczty z C++, to jednak zacznę od innych sposobów wysyłania w celu ułatwienia zrozumienia działania
protokołu SMTP.
Wysyłanie maila przy pomocy narzędzia telnet
Aby wysłać maila, można skorzystać z prostego programu
telnet,łącząc się na serwer pocztowy na odpowiedni port (domyślnie SMTP używa portu 25). Jednakże proszę pamiętać, że narzędzie to
nie stosuje szyfrowania. Co ciekawe można wysłać maila bez logowania z danego serwera, ale nie każdy serwer na coś takiego pozwala.
Akurat testowałem artykuł na poczcie o2, z której w następujący sposób można wysłać maila (z numerkiem są odpowiedzi serwera, moje komendy DUŻYMI)
Poniższy sposób jest nieszyfrowany! |
telnet poczta.o2.pl 25
Trying 193.17.41.99...
Connected to poczta.o2.pl.
Escape character is '^]'.
220 smtp.tlen.pl ESMTP
HELO smtp.tlen.pl
250 smtp.tlen.pl
MAIL FROM ktos@o2.pl
250 ok
RCPT TO: ktos@o2.pl
553 SMTP auth required (#5.7.1) / Wymagana autoryzacja SMTP - zobacz strone: http://poczta.wp.pl/autoryzacja
AUTH LOGIN
334 VXNlcm5hbWU6
a3Rvcw==
334 UGFzc3dvcmQ6
aGFzbG8=
235 go ahead
MAIL FROM: ktos@o2.pl
250 ok
RCPT TO: ktos@o2.pl
250 ok
DATA
354 go ahead
SUBJECT: Wiadomosc telnet
FROM: ktos@o2.pl
A teraz sie zabawimy w terminalu
ale nie przesadzajmy
.
250 ok 1577138129 qp 31226
quit
221 smtp.tlen.pl
Connection closed by foreign host.
Te dziwne znaczki to kodowanie
Base64, które znaki nieangielskie koduje przy pomocy znaków angielskich, to nie jest szyfrowanie! Aby przekonwertować na Base64 możemy użyć na szybko
perl -MMIME::Base64 -e 'print encode_base64("tekstDoZakodowania");'
, nie polecam używania
konwerterów on-line, gdyż powyżej zakodowane są nasz login i hasło. Jak widać, aby wysłać maila przy pomocy serwera poczty O2/WP (ten sam serwer) konieczne jest zarówno logowanie, jak i podanie nadawcy wiadomości w wiadomości.
Wysyłanie maila z konsoli przy pomocy istniejących programów
Maila można wysłać prosto przy pomocy linuxowej funkcji
mail
. Zależnie od konfiguracji możemy to zrobić w następujący sposób (gdy skonfigurujemy program mail):
echo "Treść wiadomości" | mail -s "temat wiadomości" odbiorca@o2.pl
, gdy go nie skonfigurujemy możemy zrobić w następujący sposób:
mail -s "Temat wiadomosci" -a'From:Ktos<ktos@o2.pl>' odbiorca@o2.pl <<< "Tekst wiadomosci"
. Maile tak wysyłane będą bardziej ochoczo trafiać do SPAMu, gdyż nie odbywa się logowanie na znany serwer poczty. A nasz komputer staje się takowym serwerem, który wysyła na inny serwer. Poza programem
mail
mamy jeszcze
mailx czy
mutt, ale nie o nich jest ten artykuł.
Wysyłanie maili z poziomu C++ przy pomocy biblioteki Poco
Jeśli mimo wszystko dotarłeś tutaj, Drogi Czytelniku, pocieszę Cię, że wysyłanie maili w C++ przy użyciu biblioteki POCO jest bardzo proste i nie wymaga dużej ilości kodu (nawet wg mnie jest prostsze niż wysyłanie z poziomu
Pythona). Niemniej jednak będę to tłumaczył, idąc przykład za przykładem, a w pełni funkcjonalny "gotowiec" znajduje się na końcu artykułu, a także w oficjalnych przykładach (biblioteki:
$(KOD_BIBLIOTEKI_POCO)/Net/samples/Mail/src/Mail.cpp
lub
$(KOD_BIBLIOTEKI_POCO)/NetSSL_OpenSSL/samples/Mail/src/Mail.cpp
).
Wysyłanie maila w formie tekstowej bez załącznika
Do tego celu potrzebujemy załączyć
Poco / Net / SMTPClientSession.h
oraz wywołać odpowiednie funkcje, które umieściłem poniżej (sygnatury funkcji są pobrane z
dokumentacji):
enum { SMTPClientSession::SMTP_PORT = 25 };
enum SMTPClientSession::LoginMethod
{
AUTH_NONE,
AUTH_CRAM_MD5,
AUTH_CRAM_SHA1,
AUTH_LOGIN,
AUTH_PLAIN,
AUTH_XOAUTH2
}
SMTPClientSession( const std::string & host, Poco::UInt16 port = SMTP_PORT );
void login( const std::string & hostname );
void login();
void login( LoginMethod loginMethod, const std::string & username, const std::string & password );
void sendMessage( const MailMessage & message );
void close();
Do wysyłki potrzebujemy jeszcze odpowiednio ustawić wiadomość e-mail, będzie to ten sam typ
Poco::MailMesssage co w
artykule o pobieraniu poczty, z tą różnicą, że tym razem zamiast pobierać informacje, będziemy je ustawiać. Oto potrzebne informacje:
enum ContentDisposition {
CONTENT_INLINE,
CONTENT_ATTACHMENT };
enum RecipientType { PRIMARY_RECIPIENT, CC_RECIPIENT, BCC_RECIPIENT };
MailRecipient::MailRecipient( RecipientType type, const std::string & address );
typedef std::vector < MailRecipient > Recipients;
void MailMesssagesetSender( const std::string & sender );
void MailMesssagesetSubject( const std::string & subject );
void setRecipients( const Recipients & recipient );
void setContent( const std::string & content, ContentTransferEncoding encoding = ENCODING_QUOTED_PRINTABLE );
Do wysłania prostej wiadomości tyle nam wystarczy, oto przykład z logowaniem:
#include <iostream>
#include <Poco/Net/SMTPClientSession.h>
#include <Poco/Net/MailMessage.h>
#include <Poco/Net/NetException.h>
using namespace std;
using namespace Poco::Net;
int main()
{
constexpr const char * smtpServerAddress = "poczta.o2.pl";
constexpr const char * userLogin = "ktos";
constexpr const char * userPassword = "haslo";
constexpr const char * senderAddress = "ktos@o2.pl";
constexpr const char * recipientAddress = senderAddress;
try
{
MailRecipient recipient1( MailRecipient::PRIMARY_RECIPIENT, recipientAddress );
MailMessage message;
message.setRecipients( { recipient1 } );
message.setSubject( "Nasz pierwszy mail z C++" );
message.setSender( senderAddress );
message.setContent( "Jak widac to nie jest takie trudne, prawda?" );
SMTPClientSession session( smtpServerAddress );
session.login( SMTPClientSession::AUTH_PLAIN, userLogin, userPassword );
session.sendMessage( message );
session.close();
}
catch( const SMTPException & e )
{
cerr << e.what() << ", message: " << e.message() << endl;
}
catch( const NetException & e )
{
cerr << e.what() << endl;
}
}
Do kompilacji potrzebujemy następujących bibliotek z poco:
-lPocoNet -lPocoFoundation -lpthread , ewentualnie jeszcze -lpthread |
Polskie znaki w wiadomościach e-mail
Zasadniczo nie trzeba zmieniać wiele, po prostu ustawić kodowanie na UTF-8 przy pomocy funkcji:
void MailMessage::setContentType( const std::string & mediaType );
w następujący sposób:
#include <iostream>
#include <Poco/Net/SMTPClientSession.h>
#include <Poco/Net/MailMessage.h>
#include <Poco/Net/NetException.h>
using namespace std;
using namespace Poco::Net;
int main()
{
constexpr const char * smtpServerAddress = "poczta.o2.pl";
constexpr const char * userLogin = "ktos";
constexpr const char * userPassword = "haslo";
constexpr const char * senderAddress = "ktos@o2.pl";
constexpr const char * recipientAddress = senderAddress;
try
{
MailRecipient recipient1( MailRecipient::PRIMARY_RECIPIENT, recipientAddress );
MailMessage message;
message.setRecipients( { recipient1 } );
message.setSubject( MailMessage::encodeWord( "Mój drugi mail z C++" ) );
message.setContentType( "text/plain; charset=utf-8;" );
message.setSender( senderAddress );
message.setContent( "Jak widać to nie jest takie trudne, nawet z polskimi znakami, prawda?" );
SMTPClientSession session( smtpServerAddress );
session.login( SMTPClientSession::AUTH_PLAIN, userLogin, userPassword );
session.sendMessage( message );
session.close();
}
catch( const SMTPException & e )
{
cerr << e.what() << ", message: " << e.message() << endl;
}
catch( const NetException & e )
{
cerr << e.what() << endl;
}
}
Wiadomość z załącznikiem
Jeśli dodajemy załącznik, wiadomość staje się wieloczęściowa, wtedy też ustawienie treści wiadomości staje się trudniejsze. Pomocne okażą się następujące funkcje:
enum MailMessage::ContentDisposition { CONTENT_INLINE, CONTENT_ATTACHMENT };
void MailMessage::addPart( const std::string & name, PartSource * pSource, ContentDisposition disposition, ContentTransferEncoding encoding );
void MailMessage::addAttachment( const std::string & name, PartSource * pSource, ContentTransferEncoding encoding = ENCODING_BASE64 );
void MailMessage::addContent( PartSource * pSource, ContentTransferEncoding encoding = ENCODING_QUOTED_PRINTABLE );
Do powyższych warto wspomnieć o typie
PartSource
, po którym dziedziczą m.in.
StringPartSource i
FilePartSource, które obudowują odpowiednio tekst lub plik, który jest potem załączany do maila.
Czas więc na przykład wysyłania maila z załącznikami:
#include <iostream>
#include <Poco/Net/SMTPClientSession.h>
#include <Poco/Net/MailMessage.h>
#include <Poco/Net/NetException.h>
#include <Poco/Net/StringPartSource.h>
#include <Poco/Net/FilePartSource.h>
using namespace std;
using namespace Poco::Net;
int main()
{
constexpr const char * smtpServerAddress = "poczta.o2.pl";
constexpr const char * userLogin = "ktos";
constexpr const char * userPassword = "haslo";
constexpr const char * senderAddress = "ktos@o2.pl";
constexpr const char * recipientAddress = senderAddress;
try
{
MailRecipient recipient1( MailRecipient::PRIMARY_RECIPIENT, recipientAddress );
MailMessage message;
message.setRecipients( { recipient1 } );
message.setSubject( MailMessage::encodeWord( "Mój mail z C++ wraz z załącznikami" ) );
message.setContentType( "multipart/mixed; charset=utf-8;" );
message.setSender( senderAddress );
message.addContent( new StringPartSource( "Jak widać teraz ustawiamy treść wiadomości w taki sposób, też do przeżycia, prawda?" ) );
message.addAttachment( "zdjecie.jpg", new FilePartSource( "/home/user/Pulpit/mojaCoreczka.jpg" ) );
message.addAttachment( "prezentacja.pps", new FilePartSource( "/home/user/Pulpit/BozaApteka.pps" ) );
message.addAttachment( "prezentacja.pdf", new FilePartSource( "/home/user/Pulpit/cpp11_warsztaty_pdf.pdf" ) );
SMTPClientSession session( smtpServerAddress );
session.login( SMTPClientSession::AUTH_PLAIN, userLogin, userPassword );
session.sendMessage( message );
session.close();
}
catch( const SMTPException & e )
{
cerr << e.what() << ", message: " << e.message() << endl;
}
catch( const NetException & e )
{
cerr << e.what() << endl;
}
}
Własne nazwy załączników
W tym celu należy użyć innego konstruktora obiektu
FilePartSource
, który przyjmuje nazwę, niestety pierwszy argument funkcji
addAttachment
nie jest nazwą załącznika, tylko nazwą części wiadomości, przykład:
message.addAttachment( "zdjęcie.jpg", new FilePartSource( "/home/user/Pulpit/mojaCóreczka.jpg", "coreczka.jpg", "application/octet-stream" ) );
Polskie znaki w nazwach załączników
Do tego wystarczy linijkę dołączającą załącznik obudować:
message.addAttachment( MailMessage::encodeWord( "zdjęcie.jpg" ), new FilePartSource( "/home/user/Pulpit/mojaCóreczka.jpg" ) );
a dla własnej nazwy załącznika z polskimi znakami:
message.addAttachment( MailMessage::encodeWord( "zdjęcie.jpg" ), new FilePartSource( "/home/user/Pulpit/mojaCóreczka.jpg", MailMessage::encodeWord( "córeczka.jpg" ), "application/octet-stream" ) );
Szyfrowanie wysyłania maili
Szyfrowanie przy pomocy mechanizmu
STARTTLS odbywa się poprzez zamianę
SMTPClientSession na
SecureSMTPClientSession i zawołanie funkcji składowej:
bool SecureSMTPClientSession::startTLS();
- funkcję tę należy wywołać po pierwszym zalogowaniu
SecureSMTPClientSession::login();
, a po rozpoczęciu połączenia TLS możemy zalogować się jako konkretny użytkownik, reszta obsługi odbywa się tak samo.
Zasadniczo od tego momentu można by napisać drugie tyle artykułu, niemniej jednak zawarłem tutaj tylko jeden pełny przykład:
#include <iostream>
#include <Poco/Net/SecureSMTPClientSession.h>
#include <Poco/Net/MailMessage.h>
#include <Poco/Net/NetException.h>
#include <Poco/Net/StringPartSource.h>
#include <Poco/Net/FilePartSource.h>
using namespace std;
using namespace Poco::Net;
int main()
{
constexpr const char * smtpServerAddress = "poczta.o2.pl";
constexpr const char * userLogin = "ktos";
constexpr const char * userPassword = "haslo";
constexpr const char * senderAddress = "ktos@o2.pl";
constexpr const char * recipientAddress = senderAddress;
try
{
MailRecipient recipient1( MailRecipient::PRIMARY_RECIPIENT, recipientAddress );
MailMessage message;
message.setRecipients( { recipient1 } );
message.setSubject( MailMessage::encodeWord( "Mój szyfrowany mail z C++" ) );
message.setContentType( "multipart/mixed; charset=utf-8;" );
message.setSender( senderAddress );
message.addContent( new StringPartSource( "Jak widać teraz ustawiamy treść wiadomości w taki sposób, też do przeżycia, prawda?" ) );
message.addAttachment( "prezentacja", new FilePartSource( "/home/user/Pulpit/cpp11_warsztaty_pdf.pdf" ) );
SecureSMTPClientSession session( smtpServerAddress );
session.login();
session.startTLS();
session.login( SMTPClientSession::AUTH_PLAIN, userLogin, userPassword );
session.sendMessage( message );
session.close();
}
catch( const SMTPException & e )
{
cerr << e.what() << ", message: " << e.message() << endl;
}
catch( const NetException & e )
{
cerr << e.what() << endl;
}
}
Powyższy kod linkujemy przy pomocy następujących flag.
-lPocoNetSSL -lPocoCrypto -lPocoUtil -lPocoNet -lPocoXML -lPocoFoundation -lssl -lcrypto -lPocoJSON -lpthread
Dodatki
Wysyłanie dodatkowych komend SMTP
Protokół SMTP oferuje również wysyłanie dodatkowych komend do serwera pocztowego. Co prawda nie wszystkie serwery muszą wszystkie komendy obsługiwać, może dlatego twórcy biblioteki POCO do dodatkowych funkcji zaimplementowali:
int sendCommand( const std::string & command, std::string & response );
Przykład:
#include <iostream>
#include <Poco/Net/SMTPClientSession.h>
#include <Poco/Net/NetException.h>
using namespace std;
using namespace Poco::Net;
int main()
{
constexpr const char * smtpServerAddress = "poczta.o2.pl";
try
{
SMTPClientSession session( smtpServerAddress );
session.login();
string response;
session.sendCommand( "HELP", response );
cout << "Response: " << response << endl;
session.sendCommand( "NOOP", response );
cout << "Response: " << response << endl;
session.sendCommand( "SIZE", response );
cout << "Response: " << response << endl;
session.close();
}
catch( const SMTPException & e )
{
cerr << e.what() << ", message: " << e.message() << endl;
}
catch( const NetException & e )
{
cerr << e.what() << endl;
}
}
Wydruk dla poczty o2:
Response: 214 please read RFC 821, RFC 1123, RFC 1651, RFC 1652, RFC 1854
Response: 250 ok
Response: 502 unimplemented (#5.5.1)
Dla dociekliwych odsyłam do
opisu dodatkowych komend SMTPKanał mailowy
Istnieje
SMTPChannel, który ustawiamy, aby wysyłał wiadomości na danego maila. Następnie możemy to przekazać do
Loggera Dzięki temu bardziej krytyczne wydarzenia możemy loggować w postaci wysłanych maili. Informacje o tym traktuję jako ciekawostkę, przykład obrazujący użycie tego kanału można znaleźć w oficjalnym przykładzie:
${KOD_BIBLIOTEKI_POCO}/Net/samples/SMTPLogger
. Aby uruchomić ten przykład, należy po zbudowaniu go, z odpowiedniego katalogu wywołać komendy:
export POCO_BASE='sciezka/gdzie/mamy/pobrana/biblioteke/'
cd ${tam/gdzie/zbudowana/biblioteka}/bin
./SMTPLogger localhost nadawca@agh.edu.pl odbiorca@agh.edu.pl
Oczywiście nie każdy serwer pozwoli na wysłanie w taki sposób, np. poczta O2 nie pozwoli, natomiast poczta AGH, z zarejestrowanego komputera pracownika, podpiętego do sieci AGH już tak.