1
\$\begingroup\$

This question is a continuation of: Sending email using libcurl - follow-up

Does this code have any problems? Can I improve it?

#include <string>
#include <sstream>
#include <iostream>
#include <ctime>
#include <algorithm>
#include <random>
#include <curl/curl.h>

class EmailAddress 
{
public:
    EmailAddress(const char *email)
        : email_{std::string(email)}
        {
        }

    EmailAddress(const std::string &email)
        : email_{email.size() ? "<"  + email + ">" : ""}
        {
        }

    EmailAddress(const std::string &email, const std::string &displayName)
        : email_{email.size() ? "<" + email + ">" : ""},
          name_{"\"" + displayName + "\""}
        {
        }

    std::string domain() const
        {
            return email_.substr(email_.find('@') + 1);
        }

    explicit operator const char *() const
        {
            return email_.c_str();
        }

    friend std::ostream &operator<<(std::ostream &out, const EmailAddress &email)
        {
            return out << email.name_ << " " << email.email_;
        }

private:
    std::string email_;
    std::string name_;
};

typedef std::vector<EmailAddress> EmailAddresses;
std::ostream &operator<<(std::ostream &out, const EmailAddresses &emailAddresses);

class Email
{
public:
    Email(const EmailAddress   &from,
          const EmailAddress   &to,
          const std::string    &subject,
          const std::string    &body,
          const EmailAddresses &cc = EmailAddresses())                
        : from_{from}
        , to_{ to }
        , cc_{cc.empty() ? EmailAddresses(1, to) : cc}
        , subject_{subject}
        , body_{body}
        {
        }

    CURLcode send(const std::string &url,
                  const std::string &userName, 
                  const std::string &password);

private:
    struct StringData {
            std::string msg;
            size_t bytesLeft;
            StringData(std::string &&m) : msg{m}, bytesLeft{msg.size()} {}
            StringData(std::string  &m) = delete;
        };

    static std::string dateTimeNow_();
    static size_t payloadSource_(void *ptr, size_t size, size_t nmemb, void *userp);
    std::string generateMessageId_() const;
    std::string setPayloadText_();

    EmailAddress from_, to_;
    EmailAddresses cc_;
    std::string subject_, body_;
};

CURLcode Email::send(const std::string &url,
                     const std::string &userName,
                     const std::string &password)
{
    CURLcode ret = CURLE_OK;

    struct curl_slist *recipients = NULL;

    CURL *curl = curl_easy_init();

    StringData textData { setPayloadText_() };

    if (curl) {
        std::ostringstream cc;
        cc << cc_;

        curl_easy_setopt(curl, CURLOPT_USERNAME,     userName.c_str());
        curl_easy_setopt(curl, CURLOPT_PASSWORD,     password.c_str());
        curl_easy_setopt(curl, CURLOPT_URL,          url     .c_str());

        curl_easy_setopt(curl, CURLOPT_USE_SSL,      (long)CURLUSESSL_ALL);
        //curl_easy_setopt(curl, CURLOPT_CAINFO,       "/path/to/certificate.pem");

        curl_easy_setopt(curl, CURLOPT_MAIL_FROM,    (const char *)from_);
        recipients = curl_slist_append(recipients,   (const char *)to_);
        recipients = curl_slist_append(recipients,   cc.str().c_str());

        curl_easy_setopt(curl, CURLOPT_MAIL_RCPT,    recipients);
        curl_easy_setopt(curl, CURLOPT_READFUNCTION, payloadSource_);
        curl_easy_setopt(curl, CURLOPT_READDATA,     &textData);
        curl_easy_setopt(curl, CURLOPT_UPLOAD,       1L);
        curl_easy_setopt(curl, CURLOPT_VERBOSE,      1L);

        ret = curl_easy_perform(curl);

        if (ret != CURLE_OK) {
            std::cerr << "curl_easy_perform() failed: "
                      << curl_easy_strerror(ret)
                      << std::endl;
        }

        curl_slist_free_all(recipients);
        curl_easy_cleanup(curl);
    }

    return ret;
}

std::string Email::dateTimeNow_()
{
    const int RFC5322_TIME_LEN = 32;

    std::string ret;
    ret.resize(RFC5322_TIME_LEN);

    time_t tt;

#ifdef _MSC_VER
    time(&tt);
    tm *t = localtime(&tt);
#else
    tm tv, *t = &tv;
    tt = time(&tt);
    localtime_r(&tt, t);
#endif

    strftime(&ret[0], RFC5322_TIME_LEN, "%a, %d %b %Y %H:%M:%S %z", t);

    return ret;
}

std::string Email::generateMessageId_() const
{
    const size_t MESSAGE_ID_LEN = 37;

    tm t;
    time_t tt;
    time(&tt);

#ifdef _MSC_VER
    gmtime_s(&t, &tt);
#else
    gmtime_r(&tt, &t);
#endif

    std::string ret;
    ret.resize(MESSAGE_ID_LEN);
    size_t dateLen = std::strftime(&ret[0], MESSAGE_ID_LEN, "%Y%m%d%H%M%S", &t);

    static const std::string alphaNum {
        "0123456789"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz" };

    std::mt19937 gen;
    std::uniform_int_distribution<> distr(0, alphaNum.length() - 1);
    std::generate_n(ret.begin() + dateLen,
                    MESSAGE_ID_LEN - dateLen,
                    [&]() { return alphaNum[distr(gen)]; });

    return ret;
}

size_t Email::payloadSource_(void *ptr, size_t size, size_t nmemb, void *userp)
{
    StringData *text = reinterpret_cast<StringData *>(userp);

    if ((size == 0) || (nmemb == 0) || ((size*nmemb) < 1) || (text->bytesLeft == 0)) {
        return 0;
    }

    if ((nmemb * size) >= text->msg.size()) {
        text->bytesLeft = 0;
        return text->msg.copy(reinterpret_cast<char *>(ptr), text->msg.size());
    }

    return 0;
}

std::string Email::setPayloadText_()
{
    std::string ret = "Date: " + dateTimeNow_() + "\r\n";

    std::ostringstream oss;
    oss << "To: "   << to_   << "\r\n"
           "From: " << from_ << "\r\n"
           "Cc: "   << cc_   << "\r\n";
    ret += oss.str();

    ret +=
        "Message-ID: <" + generateMessageId_() + "@" + from_.domain() + ">\r\n"
        "Subject: " + subject_ + "\r\n"
        "\r\n" +
        body_ + "\r\n"
        "\r\n";

    return ret;
}

std::ostream &operator<<(std::ostream &out, const EmailAddresses &emailAddresses)
{
    if (!emailAddresses.empty()) {
        auto &it = emailAddresses.begin();
        out << *it;

        while (++it != emailAddresses.end()) {
            out << "," << *it;
        }
    }

    return out;
}

int main()
{
    Email email({ "from@email.x", "FromName" },
                  "to@email.x",
                  "Subject",
                  "Body"  );

    email.send (  "smtp://smtp.email.x:25",
                  "UserName",
                  "Password"  );
}
\$\endgroup\$
3
\$\begingroup\$

A quick note regarding long messages handling in your payloadSource_ function as I stumbled upon the same problem. The problem is that if your message is too long (long body or attachment in base64 coding for instance), then (nmemb * size) >= text->msg.size() is always false and you return 0 without copying anything to the curl buffer. You then send an empty mail instead of your message.

I managed to solve that by replacing text->msg by a buffer from which I can read bytes, and changing the payloadSource_ function to something like this:

StringData*text=reinterpret_cast<StringData*>(userp);
size_t len=std::min(size*nmemb,text->bytesleft);
if(size*nmemb<1){return 0;}
if(text->bytesleft>0){
    text->bytesleft-=std::min(len,text->bytesleft);
    text->in.read(reinterpret_cast<byte*>(ptr),len);
    return len;}
return 0;

text->in is an object containing a buffer with the message (like a char*) and a pointer to the position in that buffer (at first, the beginning of that buffer).

in.read reads len bytes from in, and stores the current position in its position pointer, so that next call to payloadSource will start reading from in at that position.

You now call payloadSource several times until you copied all your message in the curl buffer and return 0 (which signals end-of-file to the library).

I had a look at the read_callback function here https://curl.haxx.se/libcurl/c/post-callback.html to help.

Hope that helps! Good luck, Cheers

V.

\$\endgroup\$
  • \$\begingroup\$ I am afraid to make a mistake. Could you make more complete example please? \$\endgroup\$ – Ufx Oct 20 '16 at 1:45
0
\$\begingroup\$

A minor nit would be that in your constructor you check for emptiness of the provided string via an implicit size() == 0

EmailAddress(const std::string &email)
    : email_{email.size() ? "<"  + email + ">" : ""}
    {
    }

It would be more descriptive to use empty()

EmailAddress(const std::string &email)
    : email_{email.empty() ? "" : "<"  + email + ">"}
    {
    }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.