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;
}
浙公網安備 33010602011771號