Adding file attachments to SMTP client for Qt5

At the begining I thought I'll need to send text only through email. But the more I thought about it, I understood that I want to add three files to the mail as well. By altering previous code I managed to add simple Multipart MIME message support. In other words file attachments.


If you are looking how to send an e-mail from Qt please refer to this article, because here I will write an introduction in MIME data headers and how to alter a simple SMTP message to inlcude file attachments.

 

Update

Both implementations are now available on github

What is MIME?

Multipurpose Internet Mail Extension (MIME) is an internet standard which improves the basic email format to support file attachments, for example audio, video, images, applications and other kind of files can be attached to the message. In addition, MIME allows formating the email in different characters sets, it's not limited to only ASCII. There are more abilities to MIME, but these were the most interesting to me.

For our extension we are going to use a special kind of MIME message with multiple parts, obsiously it's called "MIME multipart message". Let's look at a simple examples.

An email without MIME would look something like this:

To: someone@mail.com 
From: me@mail.com 
Subject: this is a subject 
And this is the body of the message

 

The same message in multipart MIME with multiple files attached to it would look something like this:

To: someone@mail.com 
From: me@mail.com 
Subject: this is a subject 
 
Mime-Version: 1.0 
Content-Type: multipart/mixed; boundary=frontier
 
--frontier <br>Content-Type: text/plain
And this is the body of the message 
 
--frontier
Content-Type: application/octet-stream 
Content-Disposition: attachment; filename=simplefile1.txt; 
Content-Transfer-Encoding: base64
VGhpcyBpcyBhIHNpbXBsZSBmaWxlIG5yMQ== 
 
--frontier
Content-Type: application/octet-stream 
Content-Disposition: attachment; filename=simplefile2.txt; 
Content-Transfer-Encoding: base64
QW5vdGhlciBmaWxlLCBucjI=
 
--frontier--

 

You should be able to see that the MIME message consists of three parts, seperated by a keyword "frontier". The keyword can be any string you like, which you specify on this line: Content-Type:multipart/mixed;boundary=frontier. Normally it's a long random string, which will be highly unlikely to occur in the message. And finally to end the message you have to but the boundary keyword in betwwen these two characters "--", as you can see in the very last line.

For every part of the message you have to specify the content type. For the body it's always going to be text/plain, however for files it can be changed, but nowdays every attachment can be specified as application/octet-stream. As for as I know it might not be the best practice, but it works in my case. For example here could be content types like: audio/basic, application/porstscript and so on. But as I said for downloadable attachments relativly anything can be specified as application/octet-stream.

For files you can see in the sample MIME message two more properties - Content-Disposition and Content-Transfer-Encoding.

Basically by adding Content-Disposition: attachment; filename=simplefile1.txt; it specifies that this part should be viewed as a downloadable file instead of viewing directly in the body of the message. Typically if a web broser will find this property, it will prompt the user to save the contents of the page, instead of showing it inline with email.

The Content-Transfer-Encoding is a MIME property to specify in what encoding the binary data will be represented other than ASCII.

A message content can be encoded with following types:

  • 7bit
  • quoted-printable
  • base64
  • 8bit (only if SMTP has 8BITMIME extension)
  • binary (only if servers support BINARYMIME extension)

Because of the integrated base64 encoding in Qt, I chose to use that. In addition, it should be supported by a standard SMTP server without any extensions. But it's just preference, you can encode your data however you like.

Basically that's all you have to know to send an email using SMTP protocol thourgh Qt5. It's all about the formating the data, the principles are the same as in my previous article.

 

Update (14/07/2013)

Thanks to lilgoldfish, I noticed that the message couldn't be received on most of the email clients.After some tweaking an re-reading the MIME message description, managed to found why is that. So if your message doesn't appear, HTML or plain, that means that your MIME data is incorrectly formated. Apperantly every single breakline is important. So if a MIME message has a new line between break string, then it should be there. Not all the email clients are as awesome as GMAIL. Another thing I noticed is, when you send a file with content type octet-stream gmail will be able to understand that it is a picture or a text file automatically. However that is not the case for yahoo. 

Anyway, I updated the sample code to accomedate my previous mistakes with MIME formating.

 

Snapshots:

 

When sending HTML formatted mails, be careful on your HTML formating. Because, there are spam filters, which will automatically assume your email is a spam if the HTML misses a tag or in other ways is formated sloppy.

 

Here is the improved code to support multiple file attachments:

smtp.h

/*
Copyright (c) 2013 Raivis Strogonovs
 
https://morf.lv
 
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.*/
 
#ifndef SMTP_H
#define SMTP_H
 
 
#include <QtNetwork/QAbstractSocket>
#include <QtNetwork/QSslSocket>
#include < QString >
#include < QTextStream >
#include < QDebug >
#include <QtWidgets/QMessageBox>
#include < QByteArray >
#include < QFile >
#include < QFileInfo >
 
 
 
class Smtp : public QObject
{
    Q_OBJECT
 
 
public:
    Smtp( const QString &user, const QString &pass,
          const QString &host, int port = 465, int timeout = 30000 );
    ~Smtp();
 
    void sendMail( const QString &from, const QString &to,
                   const QString &subject, const QString &body,
                   QStringList files = QStringList());
 
signals:
    void status( const QString &);
 
private slots:
    void stateChanged(QAbstractSocket::SocketState socketState);
    void errorReceived(QAbstractSocket::SocketError socketError);
    void disconnected();
    void connected();
    void readyRead();
 
private:
    int timeout;
    QString message;
    QTextStream *t;
    QSslSocket *socket;
    QString from;
    QString rcpt;
    QString response;
    QString user;
    QString pass;
    QString host;
    int port;
    enum states{Tls, HandShake ,Auth,User,Pass,Rcpt,Mail,Data,Init,Body,Quit,Close};
    int state;
 
};
#endif

smtp.cpp

/*
Copyright (c) 2013 Raivis Strogonovs
 
https://morf.lv
 
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.*/
 
 
 
#include "smtp.h"
 
Smtp::Smtp( const QString &user, const QString &pass, const QString &host, int port, int timeout )
{    
    socket = new QSslSocket(this);
 
    connect(socket, SIGNAL(readyRead()), this, SLOT(readyRead()));
    connect(socket, SIGNAL(connected()), this, SLOT(connected() ) );
    connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this,SLOT(errorReceived(QAbstractSocket::SocketError)));   
    connect(socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(stateChanged(QAbstractSocket::SocketState)));
    connect(socket, SIGNAL(disconnected()), this,SLOT(disconnected()));
 
 
    this->user = user;
    this->pass = pass;
 
    this->host = host;
    this->port = port;
    this->timeout = timeout;
 
 
}
 
void Smtp::sendMail(const QString &from, const QString &to, const QString &subject, const QString &body, QStringList files)
{
    message = "To: " + to + "\n";    
    message.append("From: " + from + "\n");
    message.append("Subject: " + subject + "\n");
 
    //Let's intitiate multipart MIME with cutting boundary "frontier"
    message.append("MIME-Version: 1.0\n");
    message.append("Content-Type: multipart/mixed; boundary=frontier\n\n");
 
 
 
    message.append( "--frontier\n" );
    //message.append( "Content-Type: text/html\n\n" );  //Uncomment this for HTML formating, coment the line below
    message.append( "Content-Type: text/plain\n\n" );
    message.append(body);
    message.append("\n\n");
 
    if(!files.isEmpty())
    {
        qDebug() << "Files to be sent: " << files.size();
        foreach(QString filePath, files)
        {
            QFile file(filePath);
            if(file.exists())
            {
                if (!file.open(QIODevice::ReadOnly))
                {
                    qDebug("Couldn't open the file");
                    QMessageBox::warning( 0, tr( "Qt Simple SMTP client" ), tr( "Couldn't open the file\n\n" )  );
                        return ;
                }
                QByteArray bytes = file.readAll();
                message.append( "--frontier\n" );
                message.append( "Content-Type: application/octet-stream\nContent-Disposition: attachment; filename="+ QFileInfo(file.fileName()).fileName() +";\nContent-Transfer-Encoding: base64\n\n" );
                message.append(bytes.toBase64());
                message.append("\n");
            }
        }
    }
    else
        qDebug() << "No attachments found";
 
 
    message.append( "--frontier--\n" );
 
    message.replace( QString::fromLatin1( "\n" ), QString::fromLatin1( "\r\n" ) );
    message.replace( QString::fromLatin1( "\r\n.\r\n" ),QString::fromLatin1( "\r\n..\r\n" ) );
 
 
    this->from = from;
    rcpt = to;
    state = Init;
    socket->connectToHostEncrypted(host, port); //"smtp.gmail.com" and 465 for gmail TLS
    if (!socket->waitForConnected(timeout)) {
         qDebug() << socket->errorString();
     }
 
    t = new QTextStream( socket );
 
 
 
}
 
Smtp::~Smtp()
{
    delete t;
    delete socket;
}
void Smtp::stateChanged(QAbstractSocket::SocketState socketState)
{
 
    qDebug() <<"stateChanged " << socketState;
}
 
void Smtp::errorReceived(QAbstractSocket::SocketError socketError)
{
    qDebug() << "error " <<socketError;
}
 
void Smtp::disconnected()
{
 
    qDebug() <<"disconneted";
    qDebug() << "error "  << socket->errorString();
}
 
void Smtp::connected()
{    
    qDebug() << "Connected ";
}
 
void Smtp::readyRead()
{
 
     qDebug() <<"readyRead";
    // SMTP is line-oriented
 
    QString responseLine;
    do
    {
        responseLine = socket->readLine();
        response += responseLine;
    }
    while ( socket->canReadLine() && responseLine[3] != ' ' );
 
    responseLine.truncate( 3 );
 
    qDebug() << "Server response code:" <<  responseLine;
    qDebug() << "Server response: " << response;
 
    if ( state == Init && responseLine == "220" )
    {
        // banner was okay, let's go on
        *t << "EHLO localhost" <<"\r\n";
        t->flush();
 
        state = HandShake;
    }
    //No need, because I'm using socket->startClienEncryption() which makes the SSL handshake for you
    /*else if (state == Tls && responseLine == "250")
    {
        // Trying AUTH
        qDebug() << "STarting Tls";
        *t << "STARTTLS" << "\r\n";
        t->flush();
        state = HandShake;
    }*/
    else if (state == HandShake && responseLine == "250")
    {
        socket->startClientEncryption();
        if(!socket->waitForEncrypted(timeout))
        {
            qDebug() << socket->errorString();
            state = Close;
        }
 
 
        //Send EHLO once again but now encrypted
 
        *t << "EHLO localhost" << "\r\n";
        t->flush();
        state = Auth;
    }
    else if (state == Auth && responseLine == "250")
    {
        // Trying AUTH
        qDebug() << "Auth";
        *t << "AUTH LOGIN" << "\r\n";
        t->flush();
        state = User;
    }
    else if (state == User && responseLine == "334")
    {
        //Trying User        
        qDebug() << "Username";
        //GMAIL is using XOAUTH2 protocol, which basically means that password and username has to be sent in base64 coding
        //https://developers.google.com/gmail/xoauth2_protocol
        *t << QByteArray().append(user).toBase64()  << "\r\n";
        t->flush();
 
        state = Pass;
    }
    else if (state == Pass && responseLine == "334")
    {
        //Trying pass
        qDebug() << "Pass";
        *t << QByteArray().append(pass).toBase64() << "\r\n";
        t->flush();
 
        state = Mail;
    }
    else if ( state == Mail && responseLine == "235" )
    {
        // HELO response was okay (well, it has to be)
 
        //Apperantly for Google it is mandatory to have MAIL FROM and RCPT email formated the following way -> <email@gmail.com>
        qDebug() << "MAIL FROM:<" << from << ">";
        *t << "MAIL FROM:<" << from << ">\r\n";
        t->flush();
        state = Rcpt;
    }
    else if ( state == Rcpt && responseLine == "250" )
    {
        //Apperantly for Google it is mandatory to have MAIL FROM and RCPT email formated the following way -> <email@gmail.com>
        *t << "RCPT TO:<" << rcpt << ">\r\n"; //r
        t->flush();
        state = Data;
    }
    else if ( state == Data && responseLine == "250" )
    {
 
        *t << "DATA\r\n";
        t->flush();
        state = Body;
    }
    else if ( state == Body && responseLine == "354" )
    {
 
        *t << message << "\r\n.\r\n";
        t->flush();
        state = Quit;
    }
    else if ( state == Quit && responseLine == "250" )
    {
 
        *t << "QUIT\r\n";
        t->flush();
        // here, we just close.
        state = Close;
        emit status( tr( "Message sent" ) );
    }
    else if ( state == Close )
    {
        deleteLater();
        return;
    }
    else
    {
        // something broke.
        QMessageBox::warning( 0, tr( "Qt Simple SMTP client" ), tr( "Unexpected reply from SMTP server:\n\n" ) + response );
        state = Close;
        emit status( tr( "Failed to send message" ) );
    }
    response = "";
}

 

Example usage:

Smtp* smtp = new Smtp(ui->uname->text(), ui->paswd->text(), ui->server->text(), ui->port->text().toInt());
connect(smtp, SIGNAL(status(QString)), this, SLOT(mailSent(QString)));
 
if( !files.isEmpty() )
    smtp->sendMail(ui->uname->text(), ui->rcpt->text() , ui->subject->text(),ui->msg->toPlainText(), files );
else
    smtp->sendMail(ui->uname->text(), ui->rcpt->text() , ui->subject->text(),ui->msg->toPlainText());

files variable is just a QStringList with path to the files you want to send.

 

The result:

 

Download: smtp_multiple_files.zip (32.23K)

Well that's all it is to send an email through Qt5 with attachments. I hope you have read how to do it as well, in my opinion it's good to know as well, instead of just copying the code.

 

References:

Jerry Peek. (2006). Multipart Messages. Available: http://rand-mh.sourceforge.net/book/overall/mulmes.html. Last accessed 11th Jul 2013.

Freeformater. (Not Available). MIME Types List. Available: http://www.freeformatter.com/mime-types-list.html. Last accessed 11th Jul 2013.

Microsoft. (2004). Sample MIME Message. Available: http://msdn.microsoft.com/en-us/library/ms526560%28v=exchg.10%29.aspx. Last accessed 11th Jul 2013.



Related Articles



ADVERTISEMENT