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连接登录smtp

telnet smtp.163.com 25

在这里插入图片描述

HELO smtp.163.com

返回OK

直接MAIL FROM 开始邮件事务: 提示要鉴权,RFC 821文档中似乎并没有提到鉴权
不过想想也合理,不鉴权怎么知道是谁发的邮件,SMTP 认证也能为了使用户避免受到更多的垃圾邮件的侵扰。
authentication is required

AUTH LOGIN

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

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

登录成功
Authentication Successful

开始邮件事务

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

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(1982年比我还老)似乎已经被RFC 2821替代了
RFC 2821

C代码实现SMTP发送邮件

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

  1. 附件的发送
  2. 适配Windows

makefile

CC_FLAG= -g
GCC=gcc
#GCC=x86_64-w64-mingw32-gcc 

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

main.o:main.c
        ${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
.PHONY:clean
clean:
        rm -f *.o smtp

reference: https://blog.csdn.net/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

/**
 * @struct smtp
 */
struct smtp
{
    const char* domain;
    const char* user_name;
    const char* password;
    const char* subject;
    const char* content;
    const 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;
};


#ifdef __cplusplus
extern "C"
{
#endif

/**
 *  使用smtp协议发送邮件。不做参数检查,外部自己处理好。
 * @param domain        域名
 * @param port          端口号
 * @param user_name     用户名
 * @param password      密码
 * @param subject       标题
 * @param content       邮件内容
 * @param to            发送目标
 * @param to_len        有多少个目标
 * @return 如果发送成功返回0,否则返回一个正数表示错误原因。
 *
 * @par Sample Code:
 * @code
 *  smtp_send("smtp.163.com",25,"xxx@163.com","mypassword","email-subject","email-content",
 *          "xxx@gmail.com");
 * @code
 *
 */
int smtp_send(const char* domain,int port,const char* user_name,const char* password,
              const char* subject,const char* content,const char** to,int to_len,const char** cc, int cc_len);


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

#endif /* SMTP_H_ */

smtp.c

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

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
// #include <winsock.h>

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

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


/**
 * 返回的错误码
 */
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
};



/**
*去前后空白符
*/
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;
}


/**
 * @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
};

/**
 *  读取smtp服务器响应并简单解析出响应状态和响应参数
 * @param sm    smtp指针
 * @return 读取正常返回0,否则返回正数表示错误原因
 */
static 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;
}

static 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;
}

static 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;
    
    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;
    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 (CST)
 *        这里直接返回在堆栈上的缓冲区,意味着只能立刻使用,一旦堆栈有变
 *        动就不能在使用了。
 * @param buffer
 * @return 返回 buffer
 */
static char* smtp_time(char* buffer)
{
    time_t t2;
    time(&t2);

    struct tm t;
    localtime_r(&t2,&t);
    // localtime_s(&t2,&t);
    
    static char* week[] = {"Sun" , "Mon" , "Tue" , "Wed" , "Thu" , "Fri" , "Sat"};
    static char* month[] = {"Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , "Jul" , "Aug" , "Sep" , "Oct" , "Nov" , "Dec"};
    // C      Date: Wed, 30 Jan 2019 05:48:24 --84199186
    // C      Date: Wed, 30 Jan 2019 22:45:26 +0800 (CST)
    sprintf(buffer,"%s, %02d %s %04d %02d:%02d:%02d %s%02d00 (CST)",
    // Linux
    week[t.tm_wday],t.tm_mday,month[t.tm_mon], t.tm_year + 1900,t.tm_hour,t.tm_min,t.tm_sec,t.tm_gmtoff >= 0? "+":"-",abs(t.tm_gmtoff / 3600));  
    // TODO: Windows  
    // week[t.tm_wday],t.tm_mday,month[t.tm_mon], t.tm_year + 1900,t.tm_hour,t.tm_min,t.tm_sec,t.__tm_gmtoff >= 0? "+":"-",abs(t.__tm_gmtoff / 3600));  // C99
    return buffer;
}

static 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);
    
    // 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[50];
    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;
}

static 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;
}

typedef int (*SMTP_FUN)(struct smtp*);
static const SMTP_FUN smtp_fun[SMTP_STATUS_MAX] = {NULL,hello,auth,send_mail,quit};

int smtp_send(const char* domain,int port,const char* user_name,const char* password,
              const char* subject,const char* content,const char** to,int to_len,const char** cc, int cc_len)
{
    struct hostent* host = gethostbyname(domain);
    if(!host)
    {
        printf("domain can not find!\n");
        return SMTP_ERROR_DOMAIN;
    }

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

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

    // 连接到服务器
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr = *(struct in_addr*)host->h_addr_list[0];
    memset(local.sin_zero,0,sizeof(local.sin_zero));

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

    printf("connect ok ,ip address %s \n",inet_ntoa(local.sin_addr));
    
    struct smtp sm = {.domain=domain,.user_name=user_name,.password=password,.subject=subject,
                      .content=base64_encode(content),.status=SMTP_STATUS_EHLO,.socket=sock_fd,.to=to,
                      .to_len=to_len,.cc=cc,.cc_len=cc_len};

    // free(sm.content);
    // 这里应该是服务器欢迎信息,如果状态不是220,直接返回
    if(smtp_read(&sm) || strcmp(sm.cmd,"220")) return SMTP_ERROR_READ;

    while(sm.status != SMTP_STATUS_NULL)
    {
        int error = smtp_fun[sm.status](&sm);
        if(error)
        {
            printf("error = %d\n",error);
            return error;
        }
    }

    close(sock_fd);

    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 "smtp.h"


//返回一个 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"
    "    -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.csdn.net/zhaoyong26/article/details/54574398
void get_option(int argc, char **argv, struct smtp* sm)
{
    char *cmd = argv[0];
    int flag = 0;
    
    /**
    d:domain
    //username
    f:from
    p:password
    t:to 
    c:cc
    s:subject
    b:content
    a:attachment
    **/
    
    /**
    char *domain = NULL;
    char *from = NULL;
    char *password = NULL;
    char *to = NULL;
    char *cc = NULL;
    char *subject = NULL;
    char *content = NULL;
    char *attachment = NULL;
    **/
    
    while (1) {
        int option_index = 0;
        struct option long_options[] =
        {
            {"help"        , 0, 0, 'h'},  //0代表没有参数
            {"domain"      , 1, 0, 'd'},
            //{"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: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 '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.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.domain,25,sm.user_name,sm.password,sm.subject,sm.content,sm.to,sm.to_len, sm.cc, sm.cc_len);
    
    printf("\n\n\n----------------end------------------\n");
    
    return EXIT_SUCCESS;
}

reference

[1]. https://datatracker.ietf.org/doc/html/rfc821
[2]. https://datatracker.ietf.org/doc/html/rfc2821

  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
主要功能: 1、可以发送带附件的邮件,附件可以是多个,附件大小限制由发送方服务器而定,暂未测试具体为多少MB 2、邮件内容和主题可以是空,但当有附件时,主题取第一个附件的文件名(不含扩展名) 3、密码验证均为base64加密 4、邮件正文和附件的数据传送方式,均为base64 5、自动解析发件箱的SMTP服务器 压缩包文件简介: base.c:包含一些基本的函数,其中有一些在此程序中并未用到,只要使用了其中的base64加密算法 mail.c:包含邮件发送、数据读取、编码转换、smtp服务器连接、ip解析等函数 mailsend.c:包含main的c源文件,mail.exe则是根据mailsend.c、mail.c、base.c编译成的,具体编译方 法可参考makefile libbase.a:make之后生成的静态库 moontalk.cfg:base.c用到的配置文件,可能没用,放在这里进攻阅读参考 mail.cfg:自定义用户的配置文件,可用可不用,用作读代码的参考 mail.exe:邮件发送的执行文件,仅有命令行模式完善了,逐步输入(直接双击)的方式还不完善 b64.exe:base64加密解密的小工具,仅供参考,mail.cfg中用到密码的地方,可以使这个工具得到。 makefile:工程编译链接文件 注意:在本地使用mingw环境开发,遵循ANSI C标准,本地有系统的工程库,但是上传的时候,把这些文件 都放在一起了,可以先参考makefile进行工程调整,如果有任何问题,请发送到邮箱moontalk@yeah.net, 技术交流,不胜感激。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值