C 语言使用 SMTP 协议发送邮件
操作系统:Windows 7 (32-bit);编译器:Tiny C Compiler 0.9.27。
0. SMTP 协议通信流程
- 与服务器端建立 TCP 连接
- 发送
HELO <name>
命令标识发件人 - 发送
AUTH LOGIN
命令开始登录 - 发送用户名(经过 Base64 编码)
- 发送密码(经过 Base64 编码)
- 发送发件人邮箱地址
MAIL FROM: <addr>
- 发送收件人邮箱地址
RCPT TO: <addr>
- 发送
DATA
命令开始发送邮件正文 - 发送邮件正文(以
\r\n.\r\n
结束) - 发送
QUIT
命令结束
注:每行命令都要以 \r\n
结尾。
1. 实现 Base64 编码
使用共用体。
#include <string.h>
// 查表
#define BASE_TAB \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
"abcdefghijklmnopqrstuvwxyz" "0123456789+/"
union base64_t {
struct {
unsigned c: 8;
unsigned b: 8;
unsigned a: 8;
} src;
struct {
unsigned d: 6;
unsigned c: 6;
unsigned b: 6;
unsigned a: 6;
} res;
};
void _base64(char *res, char *src) {
union base64_t data;
data.src.a = (unsigned)src[0];
data.src.b = (unsigned)src[1];
data.src.c = (unsigned)src[2];
res[0] = BASE_TAB[data.res.a];
res[1] = BASE_TAB[data.res.b];
res[2] = BASE_TAB[data.res.c];
res[3] = BASE_TAB[data.res.d];
}
void base64(char *res, char *src) {
int len = strlen(src);
// src 中的每 3 个字符做一次操作
while (*src != 0) {
_base64(res, src);
// 若 len 不是 3 的倍数, 则 res 末尾会有若干个 0
src += 3, res += 4;
}
// 填充 '='
if (len % 3 != 0) {
// 先回退
res -= (3 - len % 3);
// 再填充
memset(res, '=', 3 - len % 3);
}
}
对于共用体中 d b c a 的顺序,可参见这篇文章。
2. 关于建立连接和发送、接收数据
将其封装为函数或宏,便于使用。
#include <string.h>
#include <winsock2.h>
#define ADDR_SIZE sizeof(struct sockaddr)
#define ADDR_OF(p) (struct sockaddr *)(p)
// 发送数据
#define snsend(sock, str, len, ...) \
do { memset(str, 0, len); \
snprintf(str, len, __VA_ARGS__); \
send(sock, str, strlen(str), 0); \
} while (0);
// 接收数据
#define snrecv(sock, str, len) \
do { memset(str, 0, len); \
recv(sock, str, len, 0); \
} while (0);
// 客户端建立服务器端连接
SOCKET client(char *ip, int port) {
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
connect(sock, ADDR_OF(&addr), ADDR_SIZE);
return sock;
}
3. 发送邮件函数
#include <stdio.h>
#include <string.h>
// 字符串和缓冲区大小
#define STR_SIZE 256
// 邮箱服务器 IP 和端口
#define SMTP_ADDR "8.8.8.8"
#define SMTP_PORT 25
typedef char string[STR_SIZE];
// 此处 pass 需要事先在函数外 Base64 编码,而 user 不需要
void send_email(char *user, char *pass, char *addr, char *body) {
SOCKET sock = 0;
string buf = {0}; // 发送缓冲区
string rbuf = {0}; // 接收缓冲区
string tmp = {0};
sock = client(SMTP_ADDR, SMTP_PORT);
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
// HELO & AUTH LOGIN
snsend(sock, buf, STR_SIZE, "HELO smtp\r\n");
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
snsend(sock, buf, STR_SIZE, "AUTH LOGIN\r\n");
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
// 用户名 & 密码
base64(tmp, user);
snsend(sock, buf, STR_SIZE, "%s\r\n", tmp);
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
snsend(sock, buf, STR_SIZE, "%s\r\n", pass);
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
// MAIL FROM & RCPT TO
snsend(sock, buf, STR_SIZE, "MAIL FROM: <%s>\r\n", user)
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
snsend(sock, buf, STR_SIZE, "RCPT TO: <%s>\r\n", addr);
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
// DATA & 邮件正文
snsend(sock, buf, STR_SIZE, "DATA\r\n");
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
snsend(sock, buf, STR_SIZE, "%s\r\n.\r\n", body);
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
// QUIT
snsend(sock, buf, STR_SIZE, "QUIT\r\n");
snrecv(sock, rbuf, STR_SIZE);
// printf("%s", rbuf); // 显示服务器返回信息
closesocket(sock);
}
4. 测试用例
int main(void) {
char tmp[STR_SIZE] = {0};
char *user = "alice@a.com";
char *pass = "password";
char *addr = "bob@b.com";
char *body = "From: \"alice\"<a@a.com>\r\n" \
"To: \"bob\"<b@b.com>\r\n" \
"Subject: Hello\r\n\r\n" \
"Hello, world!";
base64(tmp, pass);
send_email(user, tmp, addr, body);
return 0;
}