前言
有时候我们想在自己的应用里添加发邮件的功能,但是很多比较底层的编程语言是不自带直接发邮件的函数的,所以使用这类编程语言要发邮件就要自己实现发邮件的功能。下面以Qt为例写一个发送邮件的demo。如果是急着用发送邮件的功能就直接翻到最下面的封装成ZSmtp类
复制代码吧,如果想搞清楚原理,就接着往下看。
分析
准备工作
打开Qt,创建一个项目,我这把项目名命名为:smtp_test
。
在smtp_test.pro
里添加一个库:
QT += network
添加这个库是为了可以使用Qt网络编程库,添加完后Ctrl
+S
保存。
用Tcp连接到smtp服务器
万事开头难,要发邮件第一步当然是要连接到邮件的服务器。首先要选一个服务器,下面以qq邮箱为例。
qq邮箱的smtp服务器地址是:smtp.qq.com
,端口是:25
。
那么用Qt应该怎么连接呢?是不是要自己实现一个连接服务器的类?不需要Qt里自带了一个封装好了的TcpSocket
类,可以用它来直接连接qq邮箱的服务器。
代码详细解释
打开mainwindow.h
,在里面添加一个头文件:
#include <QTcpSocket>
然后添加一个私有成员变量:
QTcpSocket *tcpSocket; //用于连接服务器以及和服务器交互
再添加三个公有槽:
public slots:
void connectToServer(); //连接到服务器
void disconnectFromServer(); //与服务器断开连接
void getMessage(); //得到消息
接下来,在mainwindow.ui
里添加三个按键,分别命名为:connectButton
disconnectButton
和sendButton
,分别用于连接、断开连接和发送消息。然后添加一个 QEditText
命名为:messageEdit
并设置为只读,然后添加一个QPlainTextEdit
命名为:sendEdit
。添加好以后把那三个按钮都转到槽,都选择clicked
槽。
然后在mainwindow.cpp
的构造函数里添加:
tcpSocket = new QTcpSocket; //实例化一个QTcpSocket对象,用于连接和交互
//连接到服务器调用connectToServer槽
connect(tcpSocket, SIGNAL(connected()), this, SLOT(connectToServer()));
//断开连接调用disconnectToServer槽
connect(tcpSocket, SIGNAL(disconnected()), this, SLOT(disconnectFromServer()));
//收到消息调用getMessage槽
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(getMessage()));
并在mainwindow.cpp
里把所有槽都实现了:
void MainWindow::connectToServer() //连接到服务器
{
//<font style=啥啥啥的 都是用于设置字体
//我是想让不同事件添加到messageEdit的文本颜色不一样才搞得这么花里胡哨的
//下面的所有槽的这种操作都是为了让程序显示得比较好看
QString info = QString("<font style='color:green'>%1<br></font>")
.arg("连接到服务器");
ui->messageEdit->append(info); //连接到服务器时在messageEdit显示出来
}
void MainWindow::disconnectFromServer() //断开连接
{
QString info = QString("<font style='color:red'>%1<br></font>")
.arg("与服务器断开连接");
ui->messageEdit->append(info); //断开连接时在messageEdit显示出来
}
void MainWindow::getMessage() //得到消息
{
//获取得到的消息
QByteArray message = tcpSocket->readAll();
//防止中文乱码
QString plainText = QString::fromLocal8Bit(message);
//如果QTextEdit用到了富文本,就要用<来替换<,用>来替换>,复制无法显示
plainText.replace("<", "<").replace(">", ">");
QString info = \
QString("<font style='color:black'>服务器:<br>%1</font>")
.arg(plainText);
//显示到messageEdit,如果QTextEdit用到了富文本,就只能用<br>来换行了
ui->messageEdit
->append(info.replace("\r", "").replace("\n", "<br>"));
}
void MainWindow::on_connectButton_clicked() //主动连接服务器
{
tcpSocket->abort();
//qq邮箱的服务器地址是smtp.qq.com,端口是25,连接到qq邮箱服务器
tcpSocket->connectToHost("smtp.qq.com", 25);
}
void MainWindow::on_disconnectButton_clicked() //主动断开连接
{
tcpSocket->disconnectFromHost();
}
void MainWindow::on_sendButton_clicked() //发送消息给服务器
{
//获取要发送的文本
QString plainText = ui->sendEdit->toPlainText();
plainText.replace("\n", "\r\n").append("\r\n");
QByteArray message = QByteArray().append(plainText);
tcpSocket->write(message);
plainText.replace("<", "<").replace(">", ">")
QString info = QString("<font style='color:blue'>我:<br>%1</font>")
.arg(plainText);
//如果QTextEdit用到了富文本,就只能用<br>来换行了
ui->messageEdit->append(info.replace("\r", "").replace("\n", "<br>"));
ui->sendEdit->clear();
}
如果你只想复制粘贴请直接看这里
按照上述步骤完成后,mainwindow.h
应该是这个样子:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpSocket>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
QTcpSocket *tcpSocket;
public slots:
void connectToServer();
void disconnectFromServer();
void getMessage();
private slots:
void on_connectButton_clicked();
void on_disconnectButton_clicked();
void on_sendButton_clicked();
};
#endif // MAINWINDOW_H
mainwindow.cpp
应该是这个样子:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
tcpSocket = new QTcpSocket;
connect(tcpSocket, SIGNAL(connected()), this, SLOT(connectToServer()));
connect(tcpSocket, SIGNAL(disconnected()), this, SLOT(disconnectFromServer()));
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(getMessage()));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::connectToServer()
{
QString info = QString("<font style='color:green'>%1<br></font>").arg("连接到服务器");
ui->messageEdit->append(info);
}
void MainWindow::disconnectFromServer()
{
QString info = QString("<font style='color:red'>%1<br></font>").arg("与服务器断开连接");
ui->messageEdit->append(info);
}
void MainWindow::getMessage()
{
QByteArray message = tcpSocket->readAll();
QString plainText = QString::fromLocal8Bit(message);
plainText.replace("<", "<").replace(">", ">");
QString info = QString("<font style='color:black'>服务器:<br>%1</font>").arg(plainText);
ui->messageEdit->append(info.replace("\r", "").replace("\n", "<br>"));
}
void MainWindow::on_connectButton_clicked()
{
tcpSocket->abort();
tcpSocket->connectToHost("smtp.qq.com", 25);
}
void MainWindow::on_disconnectButton_clicked()
{
tcpSocket->disconnectFromHost();
}
void MainWindow::on_sendButton_clicked()
{
QString plainText = ui->sendEdit->toPlainText();
plainText.replace("\n", "\r\n").append("\r\n");
QByteArray message = QByteArray().append(plainText);
tcpSocket->write(message);
plainText.replace("<", "<").replace(">", ">");
QString info = QString("<font style='color:blue'>我:<br>%1</font>").arg(plainText);
ui->messageEdit->append(info.replace("\r", "").replace("\n", "<br>"));
ui->sendEdit->clear();
}
什么?你连mainwindow.ui
都懒得自己弄?直接上文本的吧:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>725</width>
<height>699</height>
</rect>
</property>
<property name="windowTitle">
<string>连接smtp服务器测试工具</string>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="4">
<widget class="QTextEdit" name="messageEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="4">
<widget class="QPlainTextEdit" name="sendEdit">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>150</height>
</size>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>400</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="connectButton">
<property name="text">
<string>connect</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="disconnectButton">
<property name="text">
<string>disconnect</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QPushButton" name="sendButton">
<property name="text">
<string>send</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>725</width>
<height>26</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusBar"/>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>
smtp_test.pro
就加一行代码都不想自己写?
QT += core gui
QT += network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = smtp_test
TEMPLATE = app
SOURCES += main.cpp\
mainwindow.cpp
HEADERS += mainwindow.h
FORMS += mainwindow.ui
要main.cpp
的我怀疑你没用过Qt。
发送邮件测试
之前的都是前戏,连接上smtp服务器只是开始,现在要用刚刚写好的那个程序来发邮件了。
先看一个例子:
点connect
打招呼
250是成功的状态码。
提出要登陆认证
输入邮箱号(要转换为base64)
输入邮箱密码(也要转换为base64,密码这种东西我就不公开了)
235是认证通过的状态码。
235 Authentication successful
表示认证通过了。
认证能通过还有一个要求,就是用于发送邮件的邮箱一定要开启smtp服务,smtp服务不开启是不可能通过认证的。
填写发件人
格式为:
MAIL FROM: <发件人邮箱号>
250是成功的状态码。
在这里
250 Ok
表示这个发件人邮箱号是存在的。
填写收件人
格式为:
RCPT TO: <收件人邮箱号>
我是为了测试方便才把发件人和收件人写成同一个的,可以不同。
250是成功的状态码。
在这里
250 Ok
表示这个收件人邮箱号是存在的。
正文开始标志
发送DATA后就可以输入要发送的内容了。
它返回的文本:
End data with <CR><LF>.<CR><LF>
的意思是用\r\n.\r\n结束。
\r\n是换行的意思,但是要注意是用\r\n换行,不是用\n换行。
填写正文
上面的Subject表示主题,即邮箱的标题,To是收件人,From是发件人,Content-Type是类型,charset是编码,Content-Transfer-Encoding是内容转移编码。具体参数都可以是什么请自行百度。到真正的正文的之前要空一行。如果你的内容里要出现\r\n.\r\n,请替换成\r\n…\r\n。其他冲突的解决方法请自行百度。
250是成功的状态码。
250 Ok: queued as
表示发送成功了(qq邮箱smtp服务器发送成功的反馈是这个,别的服务器不一定,但250是一定会有的)。
退出连接
完整的交互文本(除了我把我的密码隐藏了,其他的都是完整的):
连接到服务器
服务器:
220 smtp.qq.com Esmtp QQ Mail Server
我:
EHLO smtp.qq.com
服务器:
250-smtp.qq.com
250-PIPELINING
250-SIZE 73400320
250-STARTTLS
250-AUTH LOGIN PLAIN
250-AUTH=LOGIN
250-MAILCOMPRESS
250 8BITMIME
我:
AUTH LOGIN
服务器:
334 VXNlcm5hbWU6
我:
NzcxNzc4MDg3QHFxLmNvbQ==
服务器:
334 UGFzc3dvcmQ6
我:
我怎么能在这里公开我的密码呢
服务器:
235 Authentication successful
我:
MAIL FROM: <771778087@qq.com>
服务器:
250 Ok
我:
RCPT TO: <771778087@qq.com>
服务器:
250 Ok
我:
DATA
服务器:
354 End data with <CR><LF>.<CR><LF>
我:
Subject:=?utf-8?B?5rWL6K+V?=
To: 771778087@qq.com
From: 771778087@qq.com <771778087@qq.com>
Content-Type: text/plain; charset=UTF8;
Content-Transfer-Encoding: 7BIT
这是一封测试邮件。
.
服务器:
250 Ok: queued as
我:
QUIT
服务器:
221 Bye
与服务器断开连接
收到的邮件:
封装成ZSmtp类
在这部分我不想再多作任何的解释,把前面都看明白了这部分不需要看,自己就可以封装了。如果你急着用发邮件功能,那就复制下面的代码(只封装了发送纯文本的功能):
下面实现的类的类名是以Z开头的,这是在模仿Qt,Qt库自带的类都是以Q开头的,我的名字拼音首字母是Z所以我写的类以Z开头。
前面内容没看的注意一下,发送邮件的邮箱一定要开启smtp服务,否则是无法发送邮件的。
ZSmtp类代码
zsmtp.h
#ifndef ZSMTP_H
#define ZSMTP_H
#include <QObject>
#include <QTcpSocket>
#define EMAIL_ERROR 0 //邮件发送失败
#define EMAIL_SUCCEED 1 //邮件发送成功
class ZSmtp : public QObject
{
Q_OBJECT
public:
explicit ZSmtp(QString serverIP = "smtp.qq.com", QObject *parent = NULL);
~ZSmtp();
void sendEmail(QString username, QString password, QString to, QString title, QString text, QString ip = "");
private:
QString serverIP;
QTcpSocket *tcpSocket;
QString username;
QString password;
QString to;
QString title;
QString text;
QByteArray serverText;
int status;
signals:
void disconnected();
void emailStatus(int status); //邮件发送的状态
void progress(double p);
public slots:
void connectToServer();
void disconnectFromServer();
void getMessage();
void sendEmailSlot(QString username, QString password, QString to, QString title, QString text);
};
#endif // ZSMTP_H
zsmtp.cpp
#include "zsmtp.h"
#include <QDebug>
ZSmtp::ZSmtp(QString serverIP, QObject *parent) : QObject(parent)
{
this->serverIP = serverIP;
tcpSocket = new QTcpSocket;
connect(tcpSocket, SIGNAL(connected()), this, SLOT(connectToServer()));
connect(tcpSocket, SIGNAL(disconnected()), this, SLOT(disconnectFromServer()));
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(getMessage()));
qDebug()<<"ZSmtp";
}
ZSmtp::~ZSmtp()
{
delete tcpSocket;
qDebug()<<"~ZSmtp";
}
void ZSmtp::sendEmail(QString username, QString password, QString to, QString title, QString text, QString ip)
{
if(ip != "")
{
serverIP = ip;
}
this->username = username;
this->password = password;
this->to = to;
this->title = title;
this->text = text;
tcpSocket->abort();
serverText.clear();
status = 0;
tcpSocket->connectToHost(serverIP, 25);
}
void ZSmtp::connectToServer()
{
qDebug()<<"connect to server";
}
void ZSmtp::disconnectFromServer()
{
qDebug()<<"disconnect from server";
emit disconnected();
}
#define STATUS_MAX 6
void ZSmtp::getMessage()
{
QByteArray curText = tcpSocket->readAll();
serverText.append(curText);
QByteArray text;
//qDebug()<<curText;
if(serverText.indexOf("Error")!=-1 || serverText.indexOf("503")!=-1)
{
//出错了
qDebug()<<"get the Error message";
emit emailStatus(EMAIL_ERROR);
tcpSocket->disconnectFromHost();
return ;
}
if(status==5 && serverText.indexOf("250")!=-1)
{
//登出
text.append("QUIT\r\n");
status = 6;
}
else if(status==4 && serverText.indexOf("354")!=-1)
{
//发信息
text.append("Subject:=?utf-8?B?");
text.append(QByteArray().append(title).toBase64());
text.append("?=\r\n");
text.append("To: ");
text.append(to);
text.append("\r\nFrom: ");
text.append(username);
text.append(" <");
text.append(username);
text.append(">\r\n");
text.append("Content-Type: text/plain; charset=UTF8;\r\n");
text.append("Content-Transfer-Encoding: 7BIT\r\n\r\n");
QString t = this->text;
t.replace("\n", "\r\n").replace("\r\n.\r\n", "\r\n..\r\n");
text.append(t);
text.append("\r\n.\r\n");
status = 5;
}
else if(status==3 && serverText.indexOf("250")!=-1)
{
//DATA
text.append("DATA\r\n");
status = 4;
}
else if(status==2 && serverText.indexOf("235")!=-1)
{
//发送方和接收方
text.append("MAIL FROM: <");
text.append(username);
text.append(">\r\n");
text.append("RCPT TO: <");
text.append(to);
text.append(">\r\n");
status = 3;
}
else if(status==1 && serverText.indexOf("334")!=-1)
{
//登录
text.append(QByteArray().append(username).toBase64());
text.append("\r\n");
text.append(QByteArray().append(password).toBase64());
text.append("\r\n");
status = 2;
}
else if(status==0 && serverText.indexOf("220")!=-1)
{
//打招呼
text.append("EHLO ");
text.append(serverIP);
text.append("\r\nAUTH LOGIN\r\n");
status = 1;
}
tcpSocket->write(text);
//qDebug()<<text;
emit progress((double)status/STATUS_MAX); //进度
if(status == 6)
{
emit emailStatus(EMAIL_SUCCEED);
tcpSocket->disconnectFromHost();
}
}
void ZSmtp::sendEmailSlot(QString username, QString password, QString to, QString title, QString text)
{
sendEmail(username, password, to, title, text);
}
ZSmtp类用法说明
首先要在你的项目的.pro文件里加上:
QT += network
然后是在你想要用发邮件功能的地方写上:
ZSmtp *smtp = new ZSmtp;//默认是qq邮箱服务器,想用别的服务器就 new ZSmtp(Smtp服务器地址)
connect(smtp, SIGNAL(disconnected()), smtp, SLOT(deleteLater())); //发送完毕自行销毁
smtp->sendEmail(发送邮箱号, 发送邮箱密码, 接收邮箱号, 邮件标题, 邮件内容);
//上面的参数都不需要转换为base64,直接写原文本就可以了
进一步,如果你想得到邮件的发送状态(成功或者失败),就这样写:
void YourClass::yourSlot(int status)
{
if(status == EMAIL_SUCCEED) //邮件发送成功
{
//do something
}
else if(status == EMAIL_ERROR)
{//邮件发送失败(原因可能是发、收件人邮箱号写错了,smtp服务器崩了等)
//do something
}
}
......
//下面是要使用发送邮件功能的地方
ZSmtp *smtp = new ZSmtp;
connect(smtp, SIGNAL(emailStatus(int)), this, SLOT(yourSlot(int)));
connect(smtp, SIGNAL(disconnected()), smtp, SLOT(deleteLater()));
smtp->sendEmail(发送邮箱号, 发送邮箱密码, 接收邮箱号, 邮件标题, 邮件内容);
如果你还想获得邮件发送的进度,就这样写:
void YourClass::yourSlot(double p) //这个p就是进度,范围是0到1
{
//do something
}
......
//下面是要使用发送邮件功能的地方
ZSmtp *smtp = new ZSmtp;
connect(smtp, SIGNAL(progress(double)), this, SLOT(yourSlot(double)));
connect(smtp, SIGNAL(disconnected()), smtp, SLOT(deleteLater()));
smtp->sendEmail(发送邮箱号, 发送邮箱密码, 接收邮箱号, 邮件标题, 邮件内容);
这个ZSmtp类是不会卡死线程的,因为它是通过信号槽来接收服务器的消息的,所以不会出现使用堵塞线程的方法来获取服务器消息卡死线程的情况。但是如果你仍然想用多线程来发邮件也是可以的:
//在.h里添加一个信号
signals:
void sendEmailSignal(QString username, QString password,
QString to, QString title, QString text);
......
//下面是要使用发送邮件功能的地方
ZSmtp *smtp = new ZSmtp;
QThread *thread = new QThread;
smtp->moveToThread(thread);
connect(this,
SIGNAL(sendEmailSignal(QString,QString,QString,QString,QString)),
smtp,
SLOT(sendEmailSlot(QString,QString,QString,QString,QString)));
connect(smtp, SIGNAL(disconnected()), smtp, SLOT(deleteLater()));
connect(smtp, SIGNAL(destroyed()), thread, SLOT(quit()));
connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
thread->start();
emit sendEmailSignal(发送邮箱号, 发送邮箱密码, 接收邮箱号, 邮件标题, 邮件内容);
现在就可以随意发送邮件了。
注:163邮箱的smtp服务器地址是:smtp.163.com,端口和qq邮箱端口一样是25。基本上smtp服务器的端口都是25。
端口是25会引发一个问题:如果你是使用云端服务器等服务器连接到smtp服务器发邮件,可能会出现无法发送的情况,因为你的云服务器可能默认是禁用25端口的,如果要在你的云服务器开启25端口请与服务器管理人员联系开启。