2018年3月28日

C++ から Email (Gmail) を送信する.

Abstract
  この投稿では,C++ から Email (Gmail) の送信を行う.C++ からメールを送信するには,socket 通信で接続を確立し,OpenSSL 等の暗号化ライブラリ通して通信する必要がある.socket 通信まで正常に動作させられれば,暗号化通信は socket を OpenSSL でラップするだけである.その後,メールサーバーと定型文をやり取りすることで,メールが送信される.本投稿ではメールサーバとして Gmail を利用する.Gmail をプログラムから利用するにあたり,予め Gmail の「安全性の低いアプリの許可:」を「有効」にする必要がある.また,2 段階認証を使用している場合は,本プログラム用にアプリケーションパスワードを発行しておく必要がある.
Method
  1) Socket 通信の確立.
  2) OpenSSL による暗号化通信.
  1), 2) の実装部を下記に示します.

 ./socket.hpp
#pragma once
#include <string>
#include <unistd.h>        //close()に必要
#include <netdb.h>        //freeaddrinfo()
#include <openssl/ssl.h>

namespace sstd{ class sockSSL; }

class sstd::sockSSL{
private:
    // for Socket
    int sock;
    std::string hostNameOrAddress; //ex: "www.google.co.jp"
    std::string service;           //ex: "http" or "80" (80 is a service number of HTTP.)
    
    // for OpenSSL
    SSL_CTX *ctx;
    SSL     *ssl;
    
public:
    sockSSL(const std::string& hostNameOrAddress_in, const std::string& service_in){
        this->hostNameOrAddress = hostNameOrAddress_in;
        this->service           = service_in;
    }
    ~sockSSL(){ close(); }
    
    bool open();
    
    int send          (const std::string& msg);
    int send_withPrint(const std::string& msg);
    // RETURN VALUES (https://www.openssl.org/docs/man1.0.2/ssl/SSL_write.html)
    // The following return values can occur:
    //   >  0: The write operation was successful, the return value is the number of bytes actually written to the TLS/SSL connection.
    //   <= 0: The write operation was not successful, because either the connection was closed, an error occurred or action must be taken by the calling process. Call SSL_get_error() with the return value ret to find out the reason.
    //         SSLv2 (deprecated) does not support a shutdown alert protocol, so it can only be detected, whether the underlying connection was closed. It cannot be checked, why the closure happened.
    //         Old documentation indicated a difference between 0 and -1, and that -1 was retryable. You should instead call SSL_get_error() to find out if it's retryable.

    std::string recv(bool& result);
    void close(); // 接続を切断する
};

 ./socket.cpp
#include "socket.hpp"
#include <sstd/sstd.hpp>

bool sstd::sockSSL::open(){
    struct addrinfo info;
    info.ai_flags    = 0;
    info.ai_family   = AF_UNSPEC;   // allow IPv4 or IPv6
    info.ai_socktype = SOCK_STREAM; // SOCK_STREAM: TCP, SOCK_DGRAM: UDP
    info.ai_protocol = 0;
    
    struct addrinfo *pInfo;
    int ret = getaddrinfo(this->hostNameOrAddress.c_str(), this->service.c_str(), &info, &pInfo );
    if(ret!=0){ sstd::pdbg("ERROR: getaddrinfo() was failed: %s\n",gai_strerror(ret)); return false; }
    
    struct addrinfo *rp; // for repeat
    
    for(rp=pInfo; rp!=NULL; rp=rp->ai_next){
        this->sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);

        if(this->sock!=-1){
            if(::connect(this->sock, rp->ai_addr, rp->ai_addrlen)==0){ break; }
            sstd::pdbg("ERROR: connect() was failed.\n");
            ::close(this->sock);
        }else{
            sstd::pdbg("ERROR: socket() was failed.\n");
        }
    }

    if(rp==NULL){
        sstd::pdbg("ERROR: All connection was failed.\n");
        freeaddrinfo(pInfo);
        return false;
    }
    freeaddrinfo(pInfo);
    
    // - init OpenSSL ------------------------------------------
        // init
    SSL_load_error_strings();
    SSL_library_init();

        // settings
//  this->ctx = SSL_CTX_new( SSLv2_client_method()); // SSLver2だけを使用する
//  this->ctx = SSL_CTX_new( SSLv3_client_method()); // SSLver3だけを使用する
//  this->ctx = SSL_CTX_new( TLSv1_client_method()); // TLSver1だけを使用する
    this->ctx = SSL_CTX_new(SSLv23_client_method()); // SSLv2,SSLv3,TLSv1すべてだけを使用する

    this->ssl = SSL_new(this->ctx);

        // beginning of the SSL connection
    SSL_set_fd(this->ssl, this->sock); // throwing socket to OpenSSL
    SSL_connect(this->ssl);
    // ----------------------------------- end of init OpenSSL -
    
    return true;
}

int sstd::sockSSL::send(const std::string& msg){
    return SSL_write(this->ssl, msg.c_str(), msg.length());
}
int sstd::sockSSL::send_withPrint(const std::string& msg){
    printf("Sending Data >> %s", msg.c_str());
    return send(msg);
}

std::string sstd::sockSSL::recv(bool& result){
    result=true;
    
    std::string recvMsg;
    char buf[1024*1024];
    for(int previous_readSize=0, readSize=1;
            previous_readSize != readSize;
            previous_readSize  = readSize    )
    {
        memset(buf, 0, sizeof(buf));
        readSize = SSL_read(this->ssl, buf, sizeof(buf)-1);
        if(readSize<=0){ break; }
        buf[readSize] = '\0';
        
        recvMsg += buf;
    }
    if(recvMsg.size()==0){ result=false; }
    return recvMsg;
}

void sstd::sockSSL::close(){
    
    // finalize of OpenSSL
    SSL_shutdown(this->ssl); // correspond to SSL_connect()
    SSL_free    (this->ssl); // correspond to SSL_new()
    SSL_CTX_free(this->ctx); // correspond to SSL_CTX_new()
    
    // disconnection of the line
    ::close(this->sock);
}

  3) Gmail サーバとの通信.
  3) の実装部を下記に示します.

 ./sendMail.hpp
#pragma once

struct sMail{
    std::string usr;     // usr name
    std::string domain;  // domain
    std::string pass;    // password
//  std::string from;    // メールの送信元。[usr]より自動生成
    std::string to;      // メールの送信先
    std::string subject; // メールの件名
    std::string data;    // メールの本文(今回はHTMLmailなので、HTMLを記述)
};

bool sendMail                  (struct sMail& mail);
bool sendMail_withPrint        (struct sMail& mail);
bool sendMail_of_HTML          (struct sMail& mail);
bool sendMail_of_HTML_withPrint(struct sMail& mail);

 ./sendMail.cpp
#include <sstd/sstd.hpp>
#include "socket.hpp"
#include "sendMail.hpp"

bool sendMail(struct sMail& mail){
    sstd::sockSSL sock(sstd::ssprintf("smtp.%s", mail.domain.c_str()).c_str(), "465");
    
    bool ret;
    if(!sock.open()){return false;}                                                   sock.recv(ret); if(!ret){return false;}
    if( sock.send("EHLO localhost\r\n"                           )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    if( sock.send("AUTH LOGIN\r\n"                               )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    if( sock.send(sstd::base64_encode(mail.usr)+"\r\n"           )<=0){return false;} sock.recv(ret); if(!ret){return false;} // mail User ID
    if( sock.send(sstd::base64_encode(mail.pass)+"\r\n"          )<=0){return false;} sock.recv(ret); if(!ret){return false;} // mail pass
    if( sock.send("MAIL FROM: <"+mail.usr+'@'+mail.domain+">\r\n")<=0){return false;} sock.recv(ret); if(!ret){return false;}
    if( sock.send("RCPT TO: <"+mail.to+">\r\n"                   )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    if( sock.send("DATA\r\n"                                     )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    
    std::string buf;
    buf  = "Subject: =?UTF-8?B?"+sstd::base64_encode(mail.subject)+"?=\r\n";
    
    buf += "Mime-Version: 1.0;\r\n";
    buf += "Content-Type: text/plain; charset=\"UTF-8\";\r\n";
    buf += "Content-Transfer-Encoding: 7bit;\r\n";
    buf += "\r\n";
    buf += mail.data+"\r\n";
    buf += ".\r\n";
    if( sock.send(buf                                            )<=0){return false;} sock.recv(ret); if(!ret){return false;}

    return true;
}
bool sendMail_withPrint(struct sMail& mail){ 〜 省略 〜 }
bool sendMail_of_HTML(struct sMail& mail){
    sstd::sockSSL sock(sstd::ssprintf("smtp.%s", mail.domain.c_str()).c_str(), "465");
    
    bool ret;
    if(!sock.open()){return false;}                                                   sock.recv(ret); if(!ret){return false;}
    if( sock.send("EHLO localhost\r\n"                           )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    if( sock.send("AUTH LOGIN\r\n"                               )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    if( sock.send(sstd::base64_encode(mail.usr)+"\r\n"           )<=0){return false;} sock.recv(ret); if(!ret){return false;} // mail User ID
    if( sock.send(sstd::base64_encode(mail.pass)+"\r\n"          )<=0){return false;} sock.recv(ret); if(!ret){return false;} // mail pass
    if( sock.send("MAIL FROM: <"+mail.usr+'@'+mail.domain+">\r\n")<=0){return false;} sock.recv(ret); if(!ret){return false;}
    if( sock.send("RCPT TO: <"+mail.to+">\r\n"                   )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    if( sock.send("DATA\r\n"                                     )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    
    std::string buf;
    buf  = "Subject: =?UTF-8?B?"+sstd::base64_encode(mail.subject)+"?=\r\n";
    
    buf += "Mime-Version: 1.0;\r\n";
    buf += "Content-Type: text/html; charset=\"UTF-8\";\r\n";
    buf += "Content-Transfer-Encoding: 7bit;\r\n";
    buf += "\r\n";
    buf += mail.data+"\r\n";
    buf += ".\r\n";
    if( sock.send(buf                                            )<=0){return false;} sock.recv(ret); if(!ret){return false;}
    
    return true;
}
bool sendMail_of_HTML_withPrint(struct sMail& mail){ 〜 省略 〜 }
Implementation
  上記実装を用いて,実際に Email を送信するコードを示す.

 ./main.cpp
#include <sstd/sstd.hpp>
#include "./sstd_socket/sendMail.hpp"

int main(){
    struct sMail mail;
    mail.usr     = "usrName";
    mail.domain  = "gmail.com";
    mail.pass    = "passWord";
    mail.to      = "usrName@domain";
    mail.subject = "TESTING_NOW_テスト投稿_マルチバイトコードも自由自在!!";

    {
        mail.data    = "This is a test of plain text mail.\r\n";
        mail.data   += "\r\n";
        mail.data   += "これは,プレーンテキストメールの送信テストです.<br/>\r\n";
        mail.data   += "■■■ <b><u>タイトル</u></b> ■■■<br/>\r\n";
        mail.data   += "<ul>\r\n";
        mail.data   += "<li>項目 1. </li>\r\n";
        mail.data   += "<li>項目 2. </li>\r\n";
        mail.data   += "</ul>\r\n";
        mail.data   += "<br/>\r\n";
        mail.data   += "<hr/>\r\n";
        mail.data   += "<br/>\r\n";
        mail.data   += "abc あいう<br/>\r\n";
        mail.data   += "<br/>\r\n";
    
        if(!sendMail          (mail)){ sstd::pdbg("ERROR: sendMail() was failed.\n"); }
//      if(!sendMail_withPrint(mail)){ sstd::pdbg("ERROR: sendMail_withPrint() was failed.\n"); }
    }
    {    
        mail.data    = "This is a test of HTML mail.<br/>";
        mail.data    = "<br/>";
        mail.data   += "これは,HTML メールの送信テストです.<br/>";
        mail.data   += "■■■ <b><u>タイトル</u></b> ■■■<br/>";
        mail.data   += "<ul>";
        mail.data   += "<li>項目 1. </li>";
        mail.data   += "<li>項目 2. </li>";
        mail.data   += "</ul>";
        mail.data   += "<br/>";
        mail.data   += "<hr/>";
        mail.data   += "<br/>";
        mail.data   += "abc あいう<br/>";
        mail.data   += "<br/>";
    
//      if(!sendMail_of_HTML          (mail)){ sstd::pdbg("ERROR: sendMail_of_HTML() was failed.\n"); }
        if(!sendMail_of_HTML_withPrint(mail)){ sstd::pdbg("ERROR: sendMail_of_HTML_withPrint() was failed.\n"); }
    }
    return 0;
}
Results
  実行環境
・Ubuntu 16.04 LTS

  実行結果
$ ./exe
220 smtp.gmail.com ESMTP XXXXXXXXXXXXXXXXXXX - gsmtp

Sending Data >> EHLO localhost
250-smtp.gmail.com at your service, [XXX.XXX.XXX.XXX]
250-SIZE 35882577
250-8BITMIME
250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-CHUNKING
250 SMTPUTF8

Sending Data >> AUTH LOGIN
334 VXNlcm5hbWU6

Sending Data >> [base64Encoded_usrName]
334 UGFzc3dvcmQ6

Sending Data >> [base64Encoded_passWord]
235 2.7.0 Accepted

Sending Data >> MAIL FROM: <[usrName]@[domain]>
250 2.1.0 OK XXXXXXXXXXXXXXXXXXX - gsmtp

Sending Data >> RCPT TO: <[mail.to]>
250 2.1.5 OK XXXXXXXXXXXXXXXXXXX - gsmtp

Sending Data >> DATA
354  Go ahead XXXXXXXXXXXXXXXXXXX - gsmtp

Sending Data >> Subject: =?UTF-8?B?VEVTVElOR19OT1df44OG44K544OI5oqV56i/X+ODnuODq+ODgeODkOOCpOODiOOCs+ODvOODieOCguiHqueUseiHquWcqCEh?=
Mime-Version: 1.0;
Content-Type: text/html; charset="UTF-8";
Content-Transfer-Encoding: 7bit;

<br/>これは,HTML メールの送信テストです.<br/>■■■ <b><u>タイトル</u></b> ■■■<br/><ul><li>項目 1. </li><li>項目 2. </li></ul><br/><hr/><br/>abc あいう<br/><br/>
.
250 2.0.0 OK 1522244454 XXXXXXXXXXXXXXXXXXX - gsmtp

受信トレイの様子

メール本文 (plain text mail)

メール本文 (HTML mail)
Summary
  plain text 及び HTML 形式のメールを送信する C++ コードを開発した.
Future work
  添付ファイルに対応する.


Appendix
  本投稿で使用したコード全体を,下記の URL に置いておく.UNIX 環境であれば,make するだけで,実験できる.現状では openssl-1.0.2n.tar.gz を同封しているが,古くなった場合は,新しい openssl と置き換える必要がある.ファイル名が "openssl*.tar.gz" であれば,makefile の設定により自動で展開しコンパイルされる.

0 件のコメント:

コメントを投稿