Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Grzegorz Bazior, doktorant AGH w Krakowie
Inne artykuły

[C++, POCO] Wysyłanie wiadomości e-mail z programu w C++

[artykuł] Artykuł opisuje w jaki sposób można wysyłać maile z poziomu języka C++ przy użyciu biblioteki POCO.

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):
C/C++
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 ); // wysyła do serwera podanego jako argument komendę EHLO  lub HELO, wyrzuca SMTPException w razie błędu z SMTP, lub NetException w przypadku ogólnego problemu z połączeniem
void login(); // jw, ale używa wartości z konstuktora
void login( LoginMethod loginMethod, const std::string & username, const std::string & password ); // logowanie konkretnego użytkownika, przy pomocy konkretnej metody logowania
void sendMessage( const MailMessage & message ); // wysyłanie wcześniej przygotowanej wiadomości, możliwe wyjątki: SMTPException i NetException
void close(); // zamyka połączenie z serwerem wysyłając komendę QUIT, możliwe wyjątki : SMTPException i NetException
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:
C/C++
enum ContentDisposition {
    CONTENT_INLINE,
    CONTENT_ATTACHMENT };
enum RecipientType { PRIMARY_RECIPIENT, CC_RECIPIENT, BCC_RECIPIENT };
MailRecipient::MailRecipient( RecipientType type, const std::string & address ); // odbiorca wiadomości
typedef std::vector < MailRecipient > Recipients;

void MailMesssagesetSender( const std::string & sender ); // ustawienie nadawcy wiadomości, może to być w formie: ktos@mail.pl lub Ktoś <ktos@mail.pl>
void MailMesssagesetSubject( const std::string & subject ); // ustawienie tematu wiadomości, aby zawierał polskie znaki należy użyć metody MailMesssage::encodeWord();
void setRecipients( const Recipients & recipient ); // usawienie odbiorcy
void setContent( const std::string & content, ContentTransferEncoding encoding = ENCODING_QUOTED_PRINTABLE ); // ustawia treść wiadomości, która nie zawiera załączników, linie powinny się kończyć CRLF (czyli "\r\n")
Do wysłania prostej wiadomości tyle nam wystarczy, oto przykład z logowaniem:
C/C++
#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:
C/C++
#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:
C/C++
enum MailMessage::ContentDisposition { CONTENT_INLINE, CONTENT_ATTACHMENT }; // w ten sposób ustawiamy, czy dany obrazek będzie załączony w treści wiadomości, czy jako załącznik
void MailMessage::addPart( const std::string & name, PartSource * pSource, ContentDisposition disposition, ContentTransferEncoding encoding ); // dodaje dane pSource jako część strony, poza tym przejmuje obowiązek zwolnienia pamięci pSource
void MailMessage::addAttachment( const std::string & name, PartSource * pSource, ContentTransferEncoding encoding = ENCODING_BASE64 ); // woła pod spodem: addPart(name, pSource, CONTENT_ATTACHMENT, encoding);
void MailMessage::addContent( PartSource * pSource, ContentTransferEncoding encoding = ENCODING_QUOTED_PRINTABLE ); // dodanie treści wiadomości przy wiadomościach z załącznikiem
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:
C/C++
#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:
C/C++
#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:
C/C++
int sendCommand( const std::string & command, std::string & response ); // wyjątki: SMTPException lub NetException
Przykład:
C/C++
#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 SMTP

Kanał 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.