C语言实现SMTP发邮件

前言

写SMTP的最初目的仅仅是为了自动给自己发邮件而已,其实Python、Java等很多语言都有现成的包直接用就可以了,非常简单,甚至Linux下直接apt install一个mail相关的包就可以直接发邮件了。刚才开始觉得用Python密码明文太不安全,Java反编译太容易防君子不防小人。最终选了C实现,然而后来才发发现直接硬编码邮件账号密码到执行文件里反编译个密码还是非常简单的事,有空再对密码做加密处理吧。这里就当是了解一下SMTP协议的一个过程。

SMTP

SMTP模型

                  +----------+                +----------+
      +------+    |          |                |          |
      | User |<-->|          |      SMTP      |          |
      +------+    |  Client- |Commands/Replies| Server-  |
      +------+    |   SMTP   |<-------------->|    SMTP  |    +------+
      | File |<-->|          |    and Mail    |          |<-->| File |
      |System|    |          |                |          |    |System|
      +------+    +----------+                +----------+    +------+
                   SMTP client                SMTP server

SMTP事务

SMTP事务分为三个步骤:

  1. 以MAIL 命令开始给出发送人
MAIL <SP> FROM:<reverse-path> <CRLF>
  1. 随后一个或多个RCPT命令,提供接收者的邮箱
RCPT <SP> TO:<forward-path> <CRLF>
  1. 然后DATA 命令给出邮件数据
 DATA <CRLF>

<CRLF>:回车换行,即\r\n
<SP>: 空格字符
<CRLF>.<CRLF>: 邮件结束标志

SMTP命令

以下是SMTP命令:

            HELO <SP> <domain> <CRLF>

            MAIL <SP> FROM:<reverse-path> <CRLF>

            RCPT <SP> TO:<forward-path> <CRLF>

            DATA <CRLF>

            RSET <CRLF>

            SEND <SP> FROM:<reverse-path> <CRLF>

            SOML <SP> FROM:<reverse-path> <CRLF>

            SAML <SP> FROM:<reverse-path> <CRLF>

            VRFY <SP> <string> <CRLF>

            EXPN <SP> <string> <CRLF>

            HELP [<SP> <string>] <CRLF>

            NOOP <CRLF>

            QUIT <CRLF>

            TURN <CRLF>

HELO <SP> <domain> <CRLF>:打开传输通道,如"HELO smtp.163.com\r\n"
QUIT <CRLF> :关闭传输通道: “QUIT \r\n”

会话的第一个命令必须是HELO,会话最后一个命令必须是QUIT命令,NOOP, HELP, EXPN, 和VRFY命令可以在会话中任何地方使用

RESET (RSET):终止当前邮件事务,已经存储的发送者、接收者、邮件数据信息都被丢弃,清除缓存区
SEND (SEND): 初始化邮件事务,邮件数据被转发到一个或多个终端。
SEND AND MAIL (SAML): 初始化邮件事务,邮件数据被转发到一个或多个终端或邮箱。
VERIFY (VRFY): 验证邮箱是否存在,如果参数是用户名,则返回一个全名(如果存在)。
NOOP (NOOP): 这个命令指示服务器收到命令后不用回复OK。

必须的命令就几个HELO/MAIL/RCPT/DATA,外加一个鉴权的AUTH LOGIN,我在163试了一下RESET命令没实现?其它的命令还没测试过
RESET command not implemented
再试试RSETRESET (RSET)
RSET

命令的返回值

I: intermediate
S: success
E: error

如连接,返回220代表连接成功,返回554代表连接出错,其它命令同理参考以下命令对应的值。
Command-Reply Sequences:

   CONNECTION ESTABLISHMENT
      S: 220
      E: 554
   EHLO or HELO
      S: 250
      E: 504, 550
   MAIL
      S: 250
      E: 552, 451, 452, 550, 553, 503
   RCPT
      S: 250, 251 (but see section 3.4 for discussion of 251 and 551)
      E: 550, 551, 552, 553, 450, 451, 452, 503, 550
   DATA
      I: 354 -> data -> S: 250
                        E: 552, 554, 451, 452
      E: 451, 554, 503
   RSET
      S: 250
   VRFY
      S: 250, 251, 252
      E: 550, 551, 553, 502, 504
   EXPN
      S: 250, 252
      E: 550, 500, 502, 504
   HELP
      S: 211, 214
      E: 502, 504
   NOOP
      S: 250
   QUIT
      S: 221

纯手动发送Email

开启邮箱smtp服务

首先从网页登录邮箱,在设置里打开SMTP服务,并设置客户端授权码
打开SMTP

使用telnet连接服务

telnet smtp.163.com 25

在这里插入图片描述

HELO smtp.163.com

返回OK

smtp鉴权(几种不同的鉴权方式)

直接MAIL FROM 开始邮件事务: 提示要鉴权,RFC 821/RFC 5321 文档中并没有提到鉴权,鉴权相关在rfc4954。
authentication is required

网上查有三种鉴权机制 AUTH PLAIN, AUTH LOGIN and AUTH CRAM-MD5,但rfc4954中好像没提到 AUTH LOGIN

AUTH LOGIN

rfc文档中没找到这个认证方式的详细说明,但微软的文档[5]有对这个认证方式的说明,还有网上确有很多文章都有提这个认证方式[4]

AUTH LOGIN

AUTH LOGIN

“dXNlcm5hbWU6"是BASE64编码后的"username:”
"UGFzc3dvcmQ6"是BASE64编码后的 “Password:”

坑:为何鉴权失败?反复确认了账号、授权密码都没问题,smtp也开了,可就是登录失败。。。
原因:用户名不能带后缀,如邮箱 test@163.com 的用户名应该是test而不是全称,test经过base64编码后是dGVzdA==,即为这里要输入的用户名。
authentication failed
Note:用户名和密码都需要经过BASE64编码后再输入到Telnet会话框中(懒得自己编码的直接百度”Base64在线编码解码“)

登录成功
Authentication Successful

AUTH PLAIN

  1. rfc提供TLS层下尝试使用[PLAIN] SASL机制进行身份验证的示例[3]
   S: 220-smtp.example.com ESMTP Server
   C: EHLO client.example.com
   S: 250-smtp.example.com Hello client.example.com
   S: 250-AUTH GSSAPI DIGEST-MD5
   S: 250-ENHANCEDSTATUSCODES
   S: 250 STARTTLS
   C: STARTTLS
   S: 220 Ready to start TLS
     ... TLS negotiation proceeds, further commands
         protected by TLS layer ...
   C: EHLO client.example.com
   S: 250-smtp.example.com Hello client.example.com
   S: 250 AUTH GSSAPI DIGEST-MD5 PLAIN
   C: AUTH PLAIN dGVzdAB0ZXN0ADEyMzQ=
   S: 235 2.7.0 Authentication successful

然而,无论是QQ邮箱还是163邮箱在TLS ready后发送指令后就遗失连接
TLS

  1. 再看RFC中提供的第一个例子
     ... TLS negotiation proceeds, further commands
         protected by TLS layer ...
   C: EHLO client.example.com
   S: 250-smtp.example.com Hello client.example.com
   S: 250 AUTH GSSAPI DIGEST-MD5 PLAIN
   C: AUTH PLAIN
    (note: there is a single space following the 334
     on the following line)
   S: 334
   C: dGVzdAB0ZXN0ADEyMzQ=
   S: 235 2.7.0 Authentication successful

注意:AUTH PLAIN 后面有一个空格。
账户密码可以在换行后加,也可以直接在AUTH PLAIN后面加(如:AUTH PLAIN AGRlbW9AcXEuY29tAHBhc3N3b3Jk)
账号密码是\0开头和拼接账号和密码的,以BASE64的形式发送给服务端:
base64("\0demo@qq.com\0password")
有Linux环境的话可以用以下命令进行转换,将以下命令的demo@qq.com替换成自己的邮箱,password替换成邮箱的受权码即:

printf "\0demo@qq.com\0password`"|base64

No TLS

(Optional) AUTH CRAM-MD5

CRAM-MD5认证方式:

  1. 客户端:发送AUTH CRAM-MD5
  2. 服务端:返回随机字符串random_challenge
  3. 客户端:计算hashValue = MD5(“password random_challenge”),再把hashValue 发给服务端校验通过则登录成功。

rfc4954中提供的例子如下:

   S: 220-smtp.example.com ESMTP Server
   C: EHLO client.example.com
   S: 250-smtp.example.com Hello client.example.com
   S: 250-AUTH DIGEST-MD5 CRAM-MD5
   S: 250-ENHANCEDSTATUSCODES
   S: 250 STARTTLS
   C: AUTH CRAM-MD5
   S: 334 PDQxOTI5NDIzNDEuMTI4Mjg0NzJAc291cmNlZm91ci5hbmRyZXcuY211LmVk
      dT4=
   C: cmpzMyBlYzNhNTlmZWQzOTVhYmExZWM2MzY3YzRmNGI0MWFjMA==
   S: 235 2.7.0 Authentication successful

看起来这种方式比另外两种方式更安全,然而QQ邮箱和163邮箱都不支持这个认证方式
AUTH CRAM-MD5

开始邮件事务

登录成功后,开始邮件事务

MAIL FROM:<xxx@163.com>
RCPT TO:<xxx@qq.com>
DATA
From: zbc<xxx@163.com>
TO: zbc<xxx@qq.com>
Subject: test
Mime-Version: 1.0
Content-Type:text/plain; charset=utf-8
Content-Transfer-Encoding: base64
dGVzdCBib2R5
Message-ID: <xxxxxxxxx.xxx@163.com>
Date: Date: Sun, 16 Jun 2019 15:08:37 +0800 (CST)
.

注意,最后一行单独一个"."表示结束
发送邮件
发送成功后收到的邮件,主体内容(DATA命令后面的内容)格式还有点错误,不过发成功了,有时间再研究MIME其它文档
email

RFC 821似乎已经被RFC 2821淘汰了,然后又被最新的RFC 5321代替了
RFC 2821

C代码实现SMTP发送邮件

已经实现了普通文本邮件的发送。
未完待续
TODO:

  1. 附件的发送

Features

  • 命令行发送邮件
  • 支持Linux和Windows平台

源码编译:
进入makefile所在目录执行make命令即可

当前makefile编译Windows文件是基于交叉编译写的,makefile配置的是x86_64-w64-mingw32-gcc编译
如果在Windows下使用的是mingw-32编译的话,需要修改makefile文件,将GCC = x86_64-w64-mingw32-gcc改为GCC = mingw32-gcc或者GCC = gcc

源码文件目录结构

smtp
├── lib
│   ├── base64.c  # base64编码实现文件
│   └── base64.h  # base64编码头文件
├── main.c        # main函数
├── makefile      # makefile文件
├── os
│   ├── linux.c   # linux下实现文件
│   └── windows.c # windows下实现文件
├── README.md
├── smtp          # Linux下编译生成的可执行文件
├── smtp.c        # smtp.c文件,包含了smtp.h中声明的函数实现
├── smtp.exe      # windows下编译生成的可执行文件
└── smtp.h        # smtp.h文件,包含了smtp.c中用到的函数声明

makefile

CC_FLAG = -g
GCC = gcc
SRC = os/linux.c
ifeq ($(OS),Windows_NT)
    GCC = x86_64-w64-mingw32-gcc
    CC_FLAG += -D_WIN32_WINNT=0x0600
    LDFLAGS += -lws2_32 -liphlpapi
	SRC = os/windows.c
endif

smtp.exe: main.o smtp.o base64.o 
	${GCC} ${CC_FLAG} -o smtp main.o smtp.o base64.o ${LDFLAGS}

main.o: main.c  ${SRC}
	${GCC} ${CC_FLAG} -c main.c -o main.o

smtp.o: smtp.h smtp.c
	${GCC} ${CC_FLAG} -c smtp.c -o smtp.o

base64.o: lib/base64.h lib/base64.c
	${GCC} ${CC_FLAG} -c lib/base64.c -o base64.o

clean:
	rm -f *.o smtp smtp.exe

reference: https://blog.youkuaiyun.com/qq_26093511/article/details/78836087
BASE64编码: https://datatracker.ietf.org/doc/html/rfc4648

lib/base64.h

/*base64.h*/  
#ifndef _BASE64_H  
#define _BASE64_H  
  
#include <stdlib.h>  
#include <string.h>  

unsigned char *base64_encode(unsigned char *str);  
  
unsigned char *base64_decode(unsigned char *code);  
  
#endif 

lib/base64.c

/*base64.c*/  
#include "base64.h"  
unsigned char *base64_encode(unsigned char *str)  
{  
    long len;  
    long str_len;  
    unsigned char *res;  
    int i,j;  
    // The Base64 Alphabet
    unsigned char *base64_table="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";  
  
    // result str length
    str_len=strlen(str);  
    if(str_len % 3 == 0)  
        len=str_len/3*4;  
    else  
        len=(str_len/3+1)*4;  
  
    res=malloc(sizeof(unsigned char)*len+1);  
    res[len]='\0';  
  
    //以3个8位字符为一组进行编码  
    for(i=0, j=0;i<len-2;j+=3,i+=4)
    {  
        res[i]=base64_table[str[j]>>2]; //取出第一个字符的前6位并找出对应的结果字符  
        res[i+1]=base64_table[(str[j]&0x3)<<4 | (str[j+1]>>4)]; //将第一个字符的后位与第二个字符的前4位进行组合并找到对应的结果字符  
        res[i+2]=base64_table[(str[j+1]&0xf)<<2 | (str[j+2]>>6)]; //将第二个字符的后4位与第三个字符的前2位组合并找出对应的结果字符  
        res[i+3]=base64_table[str[j+2]&0x3f]; //取出第三个字符的后6位并找出结果字符  
    }  
  
    switch(str_len % 3)  
    {  
        case 1:  
            res[i-2]='=';  
            res[i-1]='=';  
            break;  
        case 2:  
            res[i-1]='=';  
            break;  
    }
    return res;  
}  


unsigned char *base64_decode(unsigned char *code)  
{  
    //根据base64表,以字符值为数组下标找到对应的十进制数据  
    int table[]={
             0,0,0,0,0,0,0,0,0,0,0,0,
    		 0,0,0,0,0,0,0,0,0,0,0,0,
    		 0,0,0,0,0,0,0,0,0,0,0,0,
    		 0,0,0,0,0,0,0,62,0,0,0,
    		 63,52,53,54,55,56,57,58,
    		 59,60,61,0,0,0,0,0,0,0,0,
    		 1,2,3,4,5,6,7,8,9,10,11,12,
    		 13,14,15,16,17,18,19,20,21,
    		 22,23,24,25,0,0,0,0,0,0,26,
    		 27,28,29,30,31,32,33,34,35,
    		 36,37,38,39,40,41,42,43,44,
    		 45,46,47,48,49,50,51
    	       };  
    long len;  
    long str_len;  
    unsigned char *res;  
    
    //result str length
    len=strlen(code);  
    //判断编码后的字符串后是否有=  
    if(strstr(code,"=="))  
        str_len=len/4*3-2;  
    else if(strstr(code,"="))  
        str_len=len/4*3-1;  
    else  
        str_len=len/4*3;  
  
    res=malloc(sizeof(unsigned char)*str_len+1);  
    res[str_len]='\0';  
  
    //以4个字符为一位进行解码  
    for(int i=0, j=0;i < len-2;j+=3,i+=4)  
    {  
        res[j]=((unsigned char)table[code[i]])<<2 | (((unsigned char)table[code[i+1]])>>4); //取出第一个字符对应base64表的十进制数的前6位与第二个字符对应base64表的十进制数的后2位进行组合  
        res[j+1]=(((unsigned char)table[code[i+1]])<<4) | (((unsigned char)table[code[i+2]])>>2); //取出第二个字符对应base64表的十进制数的后4位与第三个字符对应bas464表的十进制数的后4位进行组合  
        res[j+2]=(((unsigned char)table[code[i+2]])<<6) | ((unsigned char)table[code[i+3]]); //取出第三个字符对应base64表的十进制数的后2位与第4个字符进行组合  
    }  
    return res;  
}

stmp.h

/**
 * @file smtp.h
 *
 */

#ifndef SMTP_H_
#define SMTP_H_


#define SMTP_BUFFER_SIZE 1024

/**
 * 返回的错误码
 */
enum SMTP_ERROR
{
    // success
    SMTP_ERROR_OK,

    // create socket fail!
    SMTP_ERROR_SOCKET,

    // connect socket fail
    SMTP_ERROR_CONNECT,

    // can not find domain
    SMTP_ERROR_DOMAIN,

    // server error
    SMTP_ERROR_READ,

    SMTP_ERROR_WRITE,

    // server status error
    SMTP_ERROR_SERVER_STATUS
};


/**
 * @enum smtp 状态
 */
enum SMTP_STATUS
{
    SMTP_STATUS_NULL,     //!< SMTP_STATUS_NULL
    SMTP_STATUS_EHLO,     //!< SMTP_STATUS_EHLO
    SMTP_STATUS_AUTH,     //!< SMTP_STATUS_AUTH
    SMTP_STATUS_SEND,     //!< SMTP_STATUS_SEND
    SMTP_STATUS_QUIT,     //!< SMTP_STATUS_QUIT
    SMTP_STATUS_MAX       //!< SMTP_STATUS_MAX
};

/**
 * @struct smtp
 */
struct smtp
{
    const char* domain;
    int port;
    unsigned char* user_name;
    unsigned char* password;
    char* subject;
    unsigned char* content;
    char** to;
    // char** to;
    int to_len;
    char ** cc;
    int cc_len;
    char ** attachment;
    int file_count;
    int status;
    int socket;
    char buffer[SMTP_BUFFER_SIZE];
    char* cmd;
    char* data;
};

int smtp_read(struct smtp* sm);

int hello(struct smtp*);
int auth(struct smtp*);
int send_mail(struct smtp*);
int quit(struct smtp*);

typedef int (*SMTP_FUN)(struct smtp*);
// const SMTP_FUN smtp_fun[SMTP_STATUS_MAX] = {NULL,hello,auth,send_mail,quit};
extern const SMTP_FUN smtp_fun[SMTP_STATUS_MAX];  // 声明,不定义

#ifdef __cplusplus
extern "C"
{
#endif

/**
 * @brief 发送邮件
 */
int smtp_send(struct smtp* sm);

#ifdef __cplusplus
}
#endif /* end of extern "C" */

#endif /* SMTP_H_ */

smtp.c

/**
 * @file smtp.c
 *
 * 
 */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// #include <unistd.h>
#include <strings.h>
#include<math.h>


#ifdef _WIN32   // For Windows-specific functions
#include <tchar.h>  
#include <winsock2.h>
#define EWOULDBLOCK WSAEWOULDBLOCK
#else           // For Unix-specific functions
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h> 
#include <netinet/in.h>
#include <sys/socket.h>
#endif

#include "smtp.h"
#include "lib/base64.h"


#define BUFFER_SIZE 128


/**
*去前后空白符
*/
void trim(char* str){
    if(str==NULL)
        return;
    char *begin=str;
    while(*begin&&(unsigned char)*begin<=32) begin++;  
    if(!*begin){
        *str=0;
        return;
    }   
    while(*str++=*begin++); 
    str-=2;
    while((unsigned char)*str--<=32);
    *(str+2)=0;
}


/* 功  能:将str字符串中的oldstr字符串替换为newstr字符串
 * 参  数:str:操作目标 oldstr:被替换者 newstr:替换者
 * 返回值:返回替换之后的字符串
 * 版  本: V0.2
 */
char *strrpc(char *str,char *oldstr,char *newstr){
    char bstr[strlen(str)];//转换缓冲区
    memset(bstr,0,sizeof(bstr));
 
    for(int i = 0;i < strlen(str);i++){
        if(!strncmp(str+i,oldstr,strlen(oldstr))){//查找目标字符串
            strcat(bstr,newstr);
            i += strlen(oldstr) - 1;
        }else{
        	strncat(bstr,str + i,1);//保存一字节进缓冲区
	    }
    }
 
    strcpy(str,bstr);
    return str;
}


/**
 *  读取smtp服务器响应并简单解析出响应状态和响应参数
 * @param sm    smtp指针
 * @return 读取正常返回0,否则返回正数表示错误原因
 */
int smtp_read(struct smtp* sm)
{
    for(;;)
    {
        int size = recv(sm->socket,sm->buffer,SMTP_BUFFER_SIZE - 1,0);
        if(size == -1)
        {
            if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
        }

        // 套结字错误或者关闭
        if(size <= 0) break;

        sm->buffer[size] = 0;
        printf("SERVER: %s\n",sm->buffer);

        // 不确定这个是否找不到,smtp协议一般都会在响应状态后跟随参数
        sm->cmd = sm->buffer;
        char* p = strchr(sm->buffer,' ');
        if(p)
        {
            *p = '\0';
            sm->data = p + 1;
        }

        return 0;
    }

    printf("smtp_read() 接收信息错误\n");

    return SMTP_ERROR_READ;
}

/**
 *  向服务器发送信息
 * @param fd            套结字
 * @param buffer        要发送的数据
 * @param buffer_size   要发送的数据长度
 * @return  如果成功反送返回0,否则返回正数表示错误原因
 */
int smtp_write(int fd,const char* buffer)
{
    int size = strlen(buffer);
    for(int send_num = 0;send_num < size; )
    {
        int error = send(fd,&buffer[send_num],size - send_num,0);
        if(error < 0)
        {
            printf("发送数据错误 errno = %d size = %d send_num = %d",errno,size, send_num);
            if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
            return SMTP_ERROR_WRITE;
        }
        else send_num += error;
    }

    return 0;
}

/**
 *  分割接收到的数据,主要是区分base64编码结果。同时sm->data节点会被修改
 * @param sm
 * @return 返回原来的sm->data。
 */
static char* explode(struct smtp* sm)
{
    char* old = sm->data;
    char* p = old;
    while(*p)
    {
        if((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
               *p == '+' || *p == '/' || *p == '=')
        {
            p++;
        }
        else
        {
            sm->data = p;
            *p = '\0';
            break;
        }
    }

    return old;
}

int hello(struct smtp* sm)
{
    // 发送HELO命令
    char buffer[256];
    memset(buffer, 0 , sizeof(buffer));
    int size = sprintf(buffer,"HELO %s\r\n",sm->domain);

    if(smtp_write(sm->socket,buffer)) return SMTP_ERROR_WRITE;

    // 服务器应该正常返回250
    if(smtp_read(sm) || strcmp(sm->cmd,"250")) return SMTP_ERROR_READ;

    sm->status = SMTP_STATUS_AUTH;

    return 0;
}

int auth(struct smtp* sm)
{
    // 发送AUTH命令,第一次接到的数据应该是base64编码后的Username,如果不是直接返回
    // 然后第二次应该是base64后的Password
    if(smtp_write(sm->socket,"AUTH LOGIN\r\n")) return SMTP_ERROR_WRITE;
    if(smtp_read(sm) || strcmp(sm->cmd,"334")) return SMTP_ERROR_READ;
    
    // username:
    char* p = explode(sm);
    char buffer[256];
    char* BASE64_USERNAME = base64_decode(p);
    int size = strlen(BASE64_USERNAME);
    strcpy(buffer, BASE64_USERNAME);
    free(BASE64_USERNAME);
    
    if(size < 0) return SMTP_ERROR_SERVER_STATUS;
    buffer[size] = 0;
    if(strcasecmp(buffer,"username:")) return SMTP_ERROR_SERVER_STATUS;
    
    unsigned char* username = base64_encode(sm->user_name);
    size = strlen(username);
    // strcat(buffer, p2);
    strcpy(buffer, username);
    free(username);
    
    if(size < 0 || size + 2 > 256) return SMTP_ERROR_WRITE;
    buffer[size++] = '\r';
    buffer[size++] = '\n';
    buffer[size] = '\0';
    
    if(smtp_write(sm->socket,buffer)) return SMTP_ERROR_WRITE;
    if(smtp_read(sm) || strcmp(sm->cmd,"334")) return SMTP_ERROR_READ;

    // Password:
    p = explode(sm);
    char* p2 = base64_decode(p);
    size = strlen(p2);
    strcpy(buffer, p2);
    
    if(size < 0) return SMTP_ERROR_SERVER_STATUS;
    buffer[size] = 0;
    
    // if(strcasecmp(buffer,"password:")) return SMTP_ERROR_SERVER_STATUS;
    if(strcasecmp(buffer,"Password:")) return SMTP_ERROR_SERVER_STATUS;
    unsigned char* psw = base64_encode(sm->password);
    
    // email password
    strcpy(buffer, psw);
    size = strlen(psw);
    if(size < 0 || size + 2 > 256) return SMTP_ERROR_WRITE;
    buffer[size++] = '\r';
    buffer[size++] = '\n';
    buffer[size] = '\0';
    
    if(smtp_write(sm->socket,buffer)) return SMTP_ERROR_WRITE;
    if(smtp_read(sm) || strcmp(sm->cmd,"235")) return SMTP_ERROR_READ;
    sm->status = SMTP_STATUS_SEND;

    return 0;
}

/**
 * 生成smtp格式的时间字符串:Wed, 30 Jan 2019 22:45:26 +0800
 *        这里直接返回在堆栈上的缓冲区,意味着只能立刻使用,一旦堆栈有变
 *        动就不能在使用了。
 * @param buffer
 * @return 返回 buffer
 */
static char* smtp_time(char* buffer) {
    time_t now;
    struct tm tm_info;
    struct tm gm_tm;
    char time_string[100];
    char timezone[28];
    int offset_minutes;

    // 获取当前时间
    time(&now);

    // 使用 localtime 和 gmtime 获取本地时间和 GMT 时间
    tm_info = *localtime(&now);
    gm_tm = *gmtime(&now);

    // 格式化日期和时间
    strftime(time_string, sizeof(time_string), "%a, %d %b %Y %H:%M:%S", &tm_info);

    // 计算时区偏移(单位:分钟)
    offset_minutes = (int)difftime(mktime(&tm_info), mktime(&gm_tm)) / 60;

    // 将偏移量格式化为时区字符串
    snprintf(timezone, sizeof(timezone), "%+03d%02d", offset_minutes / 60, abs(offset_minutes) % 60);

    // 将格式化后的时间和时区信息拷贝到 buffer 中
    snprintf(buffer, BUFFER_SIZE, "%s %s", time_string, timezone);

    return buffer;
}

int send_mail(struct smtp* sm)
{
    // MAIL FROM
    char buffer[256];
    int size = sprintf(buffer,"MAIL FROM: <%s>\r\n",sm->user_name);
    if(smtp_write(sm->socket,buffer)) return SMTP_ERROR_WRITE;
    if(smtp_read(sm) || strcmp(sm->cmd,"250")) return SMTP_ERROR_READ;

    // RCPT TO
    int i;
    for(i = 0; i < sm->to_len; i++)
    {
        size = sprintf(buffer,"RCPT TO: <%s>\r\n",sm->to[i]);
        if(smtp_write(sm->socket,buffer)) return SMTP_ERROR_WRITE;
        if(smtp_read(sm) || strcmp(sm->cmd,"250")) return SMTP_ERROR_READ;
    }

    // DATA,最后一行是 "\r\n.\r\n" 表示邮件结束
    if(smtp_write(sm->socket,"DATA\r\n")) return SMTP_ERROR_WRITE;
    if(smtp_read(sm) || strcmp(sm->cmd,"354")) return SMTP_ERROR_READ;

    // 分配足够大缓冲区存储邮件头,唯一不确定的就是群发数量,这里先统计一下发送目标占用的字节大小
    int to_size = 0;
    for(i = 0; i < sm->to_len; i++) to_size += strlen(sm->to[i]);

    char header[to_size + 512 + strlen(sm->user_name)];
    
    // From
    char * from = (char*)malloc(sizeof(char)*(strlen("From: %s<%s>\r\n")+strlen(sm->user_name)+strlen(sm->user_name)));
    int pos = sprintf(from,"From: %s<%s>\r\n",sm->user_name,sm->user_name);
    //sprintf(&header[pos],"From: %s<%s>\r\n",sm->user_name,sm->user_name);
    // int pos = strlen("MIME-Version: 1.0\r\nContent-Type: text/html\r\n");
    memcpy(header,from,pos);
    
    // To:
    for(i = 0; i < sm->to_len; i++)
    {
        pos += sprintf(&header[pos],"To: %s\r\n",sm->to[i]);
    }
    
    // CC: TODO
    if(sm->cc != NULL && sm->cc_len > 0)
    {
        for(i = 0; i < sm->cc_len; i++)
        {
            pos += sprintf(&header[pos],"Cc: %s\r\n",sm->cc[i]);
        }
    }
    
    // Subject:
    pos += sprintf(&header[pos],"Subject: %s\r\n",sm->subject);
    
    // char mime_version[] = "Mime-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n";
    char mime_version[] = "Mime-Version: 1.0\r\nContent-Type: text/html; charset=utf-8\r\n";
    
    // pos += sprintf(&header[pos], mime_version);
    pos += snprintf(&header[pos], sizeof(header) - pos, "%s", mime_version);
    
    // Content-Transfer-Encoding: base64
    pos += sprintf(&header[pos],"Content-Transfer-Encoding: base64\r\n");
    pos += sprintf(&header[pos],"Message-ID: <%ld.%s>\r\n",time(NULL),sm->user_name);
    
    char date[128];
    pos += sprintf(&header[pos],"Date: %s\r\n\r\n",smtp_time(date));
    free(from);
    
    if(smtp_write(sm->socket,header)) return SMTP_ERROR_WRITE;
    if(smtp_write(sm->socket,sm->content)) return SMTP_ERROR_WRITE;
    if(smtp_write(sm->socket,"\r\n.\r\n")) return SMTP_ERROR_WRITE;
    if(smtp_read(sm) || strcmp(sm->cmd,"250")) return SMTP_ERROR_READ;

    sm->status = SMTP_STATUS_QUIT;
    return 0;
}

int quit(struct smtp* sm)
{
    // if(smtp_write(sm->socket,"QUIT \r\n",strlen("QUIT \r\n"))) return SMTP_ERROR_WRITE;
    if(smtp_write(sm->socket,"QUIT \r\n")) return SMTP_ERROR_WRITE;
    if(smtp_read(sm) || strcmp(sm->cmd,"221")) return SMTP_ERROR_READ;

    sm->status = SMTP_STATUS_NULL;

    return 0;
}

const SMTP_FUN smtp_fun[SMTP_STATUS_MAX] = {NULL, hello, auth, send_mail, quit}; 

os/linux.c

#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>

#include "../smtp.h"
#include "../lib/base64.h" 

int smtp_send(struct smtp* sm)
{
    struct hostent* host;
    struct sockaddr_in server;
    int sock_fd;

    // host = gethostbyname(domain);
    host = gethostbyname(sm->domain);
    if (!host) {
        printf("domain can not find!\n");
        return SMTP_ERROR_DOMAIN;
    }

    if (host->h_addrtype != AF_INET) {
        printf("address type is not support %d\n", host->h_addrtype);
        return SMTP_ERROR_DOMAIN;
    }

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        printf("can not create socket!\n");
        return SMTP_ERROR_SOCKET;
    }

    server.sin_family = AF_INET;
    // server.sin_port = htons(port);
    server.sin_port = htons(sm->port);
    server.sin_addr = *(struct in_addr*)host->h_addr_list[0];
    memset(&(server.sin_zero), 0, 8);

    if (connect(sock_fd, (struct sockaddr*)&server, sizeof(struct sockaddr)) == -1) {
        printf("can not connect socket!\n");
        close(sock_fd);
        return SMTP_ERROR_CONNECT;
    }

    printf("connect success ,ip address %s\n", inet_ntoa(server.sin_addr));
    unsigned char *encodedContent = base64_encode(sm->content);

    sm->status = SMTP_STATUS_EHLO;
    sm->socket = sock_fd;
    sm->content = encodedContent;

    if (smtp_read(sm) || strcmp(sm->cmd, "220")) {
        close(sock_fd);
        free(encodedContent);
        return SMTP_ERROR_READ;
    }

    while (sm->status != SMTP_STATUS_NULL) {
        int error = smtp_fun[sm->status](sm);
        if (error) {
            printf("error = %d\n", error);
            close(sock_fd);
            free(encodedContent);
            return error;
        }
    }

    close(sock_fd);
    free(encodedContent);
    return 0;
}

os/windows.c

#include <winsock2.h>
#include <windows.h>

#include "../smtp.h"
#include "../lib/base64.h" 

int smtp_send(struct smtp *sm)
{
    SetConsoleOutputCP(CP_UTF8); // 设置控制台为UTF-8
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("WSAStartup failed!\n");
        return SMTP_ERROR_SOCKET;
    }

    struct hostent* host;
    struct sockaddr_in server;
    int sock_fd;

    host = gethostbyname(sm->domain);
    if (!host) {
        printf("domain can not find!\n");
        WSACleanup();
        return SMTP_ERROR_DOMAIN;
    }

    if (host->h_addrtype != AF_INET) {
        printf("address type is not support %d\n", host->h_addrtype);
        WSACleanup();
        return SMTP_ERROR_DOMAIN;
    }

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        printf("can not create socket!\n");
        WSACleanup();
        return SMTP_ERROR_SOCKET;
    }

    server.sin_family = AF_INET;
    server.sin_port = htons(sm->port);
    server.sin_addr = *(struct in_addr*)host->h_addr_list[0];
    memset(&(server.sin_zero), 0, 8);

    if (connect(sock_fd, (struct sockaddr*)&server, sizeof(struct sockaddr)) == -1) {
        printf("can not connect socket!\n");
        closesocket(sock_fd);
        WSACleanup();
        return SMTP_ERROR_CONNECT;
    }

    printf("connect success ,ip address %s\n", inet_ntoa(server.sin_addr));
    unsigned char *encodedContent = base64_encode(sm->content);

    sm->status = SMTP_STATUS_EHLO;
    sm->socket = sock_fd;
    sm->content = encodedContent;

    if (smtp_read(sm) || strcmp(sm->cmd, "220")) {
        closesocket(sock_fd);
        free(encodedContent);
        WSACleanup();
        return SMTP_ERROR_READ;
    }

    while (sm->status != SMTP_STATUS_NULL) {
        int error = smtp_fun[sm->status](sm);
        if (error) {
            printf("error = %d\n", error);
            closesocket(sock_fd);
            free(encodedContent);
            WSACleanup();
            return error;
        }
    }

    closesocket(sock_fd);
    free(encodedContent);
    WSACleanup();
    return 0;
}

main.c

/**
 * @file main.c
 *
 *  Author: zbc
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
#include <errno.h>

#include "smtp.h"

#ifdef _WIN32
#include "os/windows.c"
#else
#include "os/linux.c"
#endif

//返回一个 char *arr[], size为返回数组的长度
char **explode(char sep, const char *str, int *size)
{
        int count = 0, i;
        for(i = 0; i < strlen(str); i++)
        {       
                if (str[i] == sep)
                {       
                        count ++;
                }
        }
 
        char **ret = calloc(++count, sizeof(char *));
 
        //int lastindex = -1;
        int lastindex = -1;
        int j = 0;
 
        for(i = 0; i < strlen(str); i++)
        {       
                if (str[i] == sep)
                {       
                    if ((i - lastindex -1) > 0)
                    {
                        ret[j] = calloc(i - lastindex, sizeof(char)); //分配子串长度+1的内存空间
                        memcpy(ret[j], str + lastindex + 1, i - lastindex - 1);
                        // if(j==0)
                        // {
                            // memcpy(ret[j], str + lastindex , i - lastindex);
                        // }
                        // else{
                            // memcpy(ret[j], str + lastindex + 1 , i - lastindex  -1 );
                        // }
                        j++;
                    }
                    lastindex = i;
                }
        }
        
        //处理最后一个子串
        //if (lastindex <= strlen(str) )
        if (lastindex <= (int)strlen(str) )
        {
            if ((strlen(str) - 1 - lastindex) > 0)
            {   
                ret[j] = calloc(strlen(str) - lastindex, sizeof(char));
                memcpy(ret[j], str + lastindex + 1, strlen(str) - 1 - lastindex);
                j++;
            }
        }
 
        *size = j;
 
        return ret;
}

void help_info()
{
    printf(
    "Usage: ./smtp [Option]\n"
    "    -h    --help             show this\n"
    "    -m    --show             Display sending info\n"
    "    -d    --domain           smtp server address[smtp.163.com]\n"
    "    -u    --user             username:Sender Email\n"
    "    -p    --password         \n"
    "    -P    --port             smtp server port[25]\n"
    "    -t    --to               Receiving Email list\n"
    "    -c    --cc               Carbon Copy Email list\n"
    "    -s    --subject          subject\n"
    "    -b    --content          email body partion\n"
    "    -f    --attachment       filname list\n"
    
    );
    exit(0);
}

//reference https://blog.youkuaiyun.com/zhaoyong26/article/details/54574398
void get_option(int argc, char **argv, struct smtp* sm)
{
    char *cmd = argv[0];
    int flag = 0;
    
    while (1) {
        int option_index = 0;
        struct option long_options[] =
        {
            {"help"        , 0, 0, 'h'},  //0代表没有参数
            {"domain"      , 1, 0, 'd'},
            {"port"        , 1, 0, 'P'},
            //{"from"        , 1, 0, 'f'},
            {"user"        , 1, 0, 'u'},
            {"password"    , 1, 0, 'p'},
            {"to"          , 1, 0, 't'}, 
            {"cc"          , 1, 0, 'c'}, 
            {"subject"     , 1, 0, 's'}, 
            {"attachment"  , 1, 0, 'f'}, 
            {"content"     , 1, 0, 'b'}, //1代表有参数 
            {"show"        , 0, 0, 'm'}
        };
        int c;
 
        c = getopt_long(argc, argv, "h:d:u:p:P:t:c:s:f:b:m",long_options, &option_index);  //注意这里的冒号,有冒号就需要加参数值,没有冒号就不用加参数值
        if (c == -1)
                break;
 
        switch (c)
        {
            case 'h':
                 //printf("help->\n\t%s",help_info);
                 help_info();
                 break;
            case 'd':
                // printf("domain: %s\n", optarg);
                // domain = (char*)calloc(strlen(optarg),sizeof(char));
                sm->domain = optarg;
                break;
 
            case 'u':
                sm->user_name = optarg;
                break;
 
            case 'p':
                sm->password = optarg;
                break;
 
            case 'P':
                // 将 optarg 转换为 int 类型
                sm->port = strtol(optarg, NULL, 10);
                if (errno == ERANGE || sm->port <= 0 || sm->port > 65535) {
                    fprintf(stderr, "[Error] Invalid port number.\n");
                    exit(1);
                }
                break;
            
            case 't':
            {
                int to_len;
                sm->to = explode(',', optarg, &to_len);
                sm->to_len = to_len;
                break;
            }
            case 'c':
            {
                int cc_len;
                // sm->cc = explode(',', optarg, &cc_len);
                (*sm).cc = explode(',', optarg, &cc_len);
                // sm->cc_len = cc_len;
                (*sm).cc_len = cc_len;
                break;
            }
            case 'f':
            {
                int count;
                sm->attachment = explode(',', optarg, &count);
                sm->file_count = count;
                // printf("\\\\TODO:Unable to send attachment\n");
                // exit(1);
                break;
            }
            case 's':
                sm->subject = optarg;
                break;
 
            case 'b':
                sm->content = optarg;
                break;
            case 'm':
                flag = 1;
                break;
            default:
                // printf("this is default!\n");
                break;
        }
    }
    
    if(sm->user_name == NULL){
        // help_info();
        fprintf(stderr,"[Error] user cannot be empty.\n");
        exit(1);
    }
    if(sm->password == NULL){
        // help_info();
        fprintf(stderr,"[Error] password cannot be empty.\n");
        exit(1);
    }
    if(sm->to == NULL){
        // printf("[Error] Receive Email address cannot be empty.\n");
        fprintf(stderr,"[Error] Receive Email address cannot be empty.\n");
        exit(1);
    }
    if(sm->subject == NULL){
        fprintf(stderr,"[Error] Subject cannot be empty.\n");
        exit(1);
    }
    //打印邮件信息
    if(flag == 1)
    {
        
        if(sm->domain != NULL){
            printf("domain:%s\n",sm->domain);
            //free(domain);
            //domain = NULL;
        }
        
        if(sm->user_name != NULL){
            printf("from:%s\n",sm->user_name);
        }
        
        if(sm->password != NULL){
            // printf("password:%s\n",sm->password);
            printf("password:******\n");
        }
        
        if(sm->to != NULL)
        {
            printf("To: ");
            for(int i = 0; i < sm->to_len; i++)
            {
                printf("<%s>",sm->to[i]);
                if(i+1 == sm->to_len)
                    printf("\n");
                else
                    printf(", ");
            }
        }
        if(sm->cc != NULL)
        {
            printf("CC: ");
            for(int i = 0; i < sm->cc_len; i++)
            {
                printf("<%s>",sm->cc[i]);
                if(i+1 == sm->cc_len)
                    printf("\n");
                else
                    printf(", ");
            }
        }
        if(sm->attachment != NULL)
        {
            printf("attachment: ");
            for(int i = 0; i < sm->file_count; i++)
            {
                printf("<%s>",sm->attachment[i]);
                if(i+1 == sm->file_count)
                    printf("\n");
                else
                    printf(", ");
            }
        }
        
    }
    if(sm->subject != NULL)
    {
        printf("subject:%s\n", sm->subject);
    }
    return;
}


int main(int argc,char** argv)
{
    struct smtp sm = {};
    sm.domain = "smtp.163.com";
    sm.port = 25;
    sm.cc = NULL;
    // sm.cc_len = NULL;
    
    int to_len = 2;

    /**
        d:domain
        //username
        p:password
        f:from
        t:to 
        c:cc
        s:subject
        b:content
        a:attachment
    **/
    
    printf("\n\n\n----------------------------------\n");
    get_option(argc , argv, &sm);
    
    smtp_send(&sm);
    
    printf("\n\n\n----------------end------------------\n");
    
    return EXIT_SUCCESS;
}

download binarry or source code:
smtp-linux-binary.tar.gz
smtp-windows-binary.zip
Source Code(.tar.gz)

reference

[1]. https://datatracker.ietf.org/doc/html/rfc821
[2]. https://datatracker.ietf.org/doc/html/rfc5321
[3]. https://datatracker.ietf.org/doc/html/rfc4954
[4]. https://www.samlogic.net/articles/smtp-commands-reference-auth.htm
[5]. [MS-XLOGIN]: Simple Mail Transfer Protocol (SMTP) AUTH LOGIN Extension

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值