用代码控制收发邮件,实现传递IP地址

        公网IP地址在网络通信的重要性不用多说。对个人小项目来说,容易获取到的是公网的IPv6地址,而这个公网v6地址经常变化,我们常常要把地址同步到网络。

        这里使用的方法是把IP地址发送到自己的邮箱 ,相比于绑定域名,脚本发送短信之类的方法,优点还是比较明显的:

  • 可以自定义发送和接受的过程
  • 不需要再注册新的账号,直接用现有的邮箱即可
  • 不限制编程语言,拓展性很强
  • 完全免费
  • ...... 

       

准备工作

前置知识
  • 想用代码控制收发邮件,需要使用smtp协议和pop协议。其中smtp协议是发送邮件用的,pop协议是读邮件用的。
  • 想用smtp和pop协议来控制邮件的收发,需要使用授权码。授权码在这里的作用相当于邮箱账号的密码。
  • 收发邮件经常要处理多种文本编码的转化,所以有现成的模块直接用就好,没有就根据编码的规则自己处理一下Byte数据即可。
  • 发送邮件是跟smtp服务器通信的过程,简单来说,也就是建立连接、发送一些特定的字符串、接收字符串来查看进度。接收邮件同样是这3个过程,不同的是和pop服务器通信。
  • 不同的邮箱对应的smtp服务器pop服务器不同,比如163邮箱的是smtp.163.com和pop3.163.com;qq邮箱的是smtp.qq.com和pop.qq.com。具体的自己搜索一下就出来了。
  • 网络通信只有地址是不行的,还要有端口号port。不加密的话smtp服务器默认用25端口,pop使用110端口。加密的话传输过程如果被拦截,那拦截的人是不知道具体内容的,更安全,这里不展开。

        这里我将演示用代码把自己的公网IPv6地址发送邮件给自己,以及用代码把邮件里的内容读取出来。这两类代码只做思路演示,没有考虑SSL加密。发送邮件的部分比较简单,而且大多有封装好的模块或者库,按照使用方法调用就好。我的重心放在读邮件的过程里。哪怕发送部分没有现成的模块,看懂了读邮件的方法,自己写一个发邮件也不难。

        废话不多说,直接来看代码。

发送IP地址到邮箱

        这里要解决的事,一是在自己的IP地址里筛选出“公网IPv6地址”,二是发送邮件到自己的邮箱。这里以Node.js为例

获取IPv6地址,调用内置的os模块,再进行筛选即可

const os = require('os');  
//获取IPv6公网地址,如果有多个,只取第一个
function getIPv6Address() {  
    const networkInterfaces = Object.values(os.networkInterfaces());  
    let tmp={};
    for(let i=0;i<networkInterfaces.length;i++){
        for(let j=0;j<networkInterfaces[i].length;j++){
                tmp=networkInterfaces[i][j];
                if (tmp.family=='IPv6' && tmp.internal == false){
                    return {public:true,ip:tmp.address}
                }
        }
    }
    return {public:false,ip:""}
}  

        发送邮件虽然没有内置模块可以用,但第三方模块一大把,实在不行再自己搓,难度也不大,对照着smtp指令表发几条指令完事。这里有别人写好的当然是直接用了。

先安装模块

npm i nodemailer

引入模块,封装为方法

const nodemailer = require('nodemailer');

function sendIPToMyMail({ host, port, secureConnection, mailSubject, mail, pass }, { public, ip }) {
    
    //连接和登录配置
    const transporter = nodemailer.createTransport({
        host,       //smtp服务器
        port,       //不加密是25,
        secureConnection,//是否加密,
        auth: {
            user: mail,//邮箱账号
            pass: pass,//邮箱授权码,注意不是密码
        }
    });

    //邮件内容配置
    const mailOptions = {
        from: mail, //发件方账号,当然是自己的邮箱账号
        to: mail, //发给自己,所以这里还是自己
        subject: mailSubject, //邮件主题
        text: JSON.stringify({ //这里只发纯文本,这样报文结构会简单很多
            time: new Date().getTime(),//顺便带上时间,虽然邮件头也有时间,但放在这里可以
            public,//IP地址是不是公网的
            ip
        }),
    };

    transporter.sendMail(mailOptions, (err, info) => {
        if (err) { console.log("发送失败 "+err); return; }
        console.log("发送成功 "+info);
    });//发送邮件

}

把它们组合起来,再加个定时器,定时扫描,如果IP没有变化,就不用发邮件;反之把新IP发送出去,更新IP。

const config = {
    mail: 'aaabbbccc@ddd.com', //自己的邮箱账号
    pass: 'eeeeffffgggghhhh',    //smtp授权码
    host: 'pop.iiii.com', //自己邮箱对应的smtp服务器
    port: 25,                //自己邮箱对应的smtp服务器端口
    secureConnection: false,     //是否加密通信
    mailSubject: "邮件主题",      //要发送的邮件主题,用于区分这些邮件和普通邮件

    scanTime:10, //多久扫描一次
}



function uploadSeverStart(config) {
    let nowIp = "";
    let ipMessage;

    setInterval(() => { //加个定时器

        ipMessage = getIPv6Address(); //获取IP
        if (ipMessage.ip != nowIp) {  //如果变化了
            sendIPToMyMail(config,ipMessage);  //发送邮件
            nowIp = ipMessage.ip; //更新IP
        }

    }, config.scanTime * 1000)
}

uploadSeverStart(config);

从邮件读出IP地址

       读邮件使用的是POP协议,POP协议常用的指令有

  • USER 邮箱名 \r\n
  • PASS 授权码 \r\n
  • STAT \r\n (作用是查看邮箱里的信件数量和总大小)
  • LIST \r\n (列出所有的邮件序号和对应的大小)
  • LIST 序号 \r\n (列出序号对应的邮件和大小)
  • RETR 序号 \r\n (读出序号对应的邮件头部和内容)
  • TOP 序号 行数 \r\n (读出对应的邮件头部和前几行)
  • DELE 序号 \r\n (标记对应的邮件删除)
  • RSET \r\n (清空删除选中的状态)
  • NOOP \r\n (空指令)
  • QUIT \r\n (结束通信)

        这些指令的使用方法很简单,先建立TCP连接,也就是弄一个Socket,然后把这些字符串发送过去就可以了。这里以Java代码为例子,同样不考虑加密。

        需要准备好这些参数:

  • name 邮箱账号
  • pass 邮箱授权码
  • mailHost 邮箱POP服务器
  • mailPort 邮箱POP服务器端口,这里不加密,所以是110号
  • mailSubject 要读取的邮件主题

先建立TCP连接,并且准备好发送和接收字符串的工具,声明几个临时变量。

// 准备通信
Socket mySocket = new Socket(mailHost, mailPort);
InputStreamReader reader = new InputStreamReader(mySocket.getInputStream());
OutputStreamWriter writer = new OutputStreamWriter(mySocket.getOutputStream());
Scanner sc = new Scanner(reader);
String tmp;
int tmpInt;
String tmpSubject;
sc.nextLine(); //连接成功固定返回 +ok pop3 ready,这里把它读掉

想读邮件前,自然要先登录

 // 登录
writer.write("USER " + name + "\r\n");
writer.flush(); //发送
sc.nextLine();//读一行,丢弃
writer.write("PASS " + pass + "\r\n");
writer.flush();
tmp = sc.nextLine();//再读一行,用变量接收
if (tmp.indexOf("-ERR") == 0){ //如果返回-ERR,说明登录失败,不用继续往下了
throw new Error("mail login fail");
}

        登录完成后,难道我们能直接直接查看目标邮件了吗?明显不行,POP没有这个指令能让我们检索邮件。按照能用的指令来看,我们接下来的目标是

  1. 获取邮件的总数
  2. 遍历邮件的头部,找到主题是我们想要的那封邮件
  3. 读取这封邮件的内容

        按照这个步骤,会发现我们接下来会使用STAT指令获取数量,使用TOP指令获取头部,使用RETR来读取内容。但TOP指令和RETR指令都会返回头部,而且TOP指令也可以读取内容。这样子功能不就重合了吗。我们可以考虑用TOP指令代替RETR指令,把第2步第3步放在一起做。这样又会多出来一个参数

  • readLine 读取邮件内容报文的前几行

        先获取数量

// 查看邮件数量
writer.write("STAT\r\n");
writer.flush();
//这个时候会返回类似+ok 6 6666
sc.next();//跳过+ok
tmpInt=sc.nextInt();//接收6,表明有6封邮件
sc.nextLine(); //读完剩下部分,以免影响接下来的部分

然后就是遍历读取邮件了

// 遍历读取所有邮件的头部和前 n 行
boolean isWantMail;
String dataString;
for (int i = tmpInt; i > 0; i--) { //从后往前遍历,这样会先遍历到比较新的邮件
    writer.write("TOP " + i + " " + readLine + "\r\n");
    writer.flush(); // 准备读 n 行
    isWantMail = false;
    tmp = sc.nextLine();
    // 读邮件头
    while (!tmp.isEmpty()) { //这层循环逐行读取邮件的头部,包含了发件人,收件人,主题,时间等等信息

        if (tmp.indexOf("Subject: ") == 0) {
            tmpSubject=tmp+"\n\r";
            tmp=sc.nextLine();
            while(!tmp.contains(":")){
                tmpSubject+=tmp+"\n\r";
                tmp=sc.nextLine();
            }
            tmpSubject=tmpSubject.substring(8);
            tmpSubject=parseMailSubject(tmpSubject);
            if(tmpSubject.equals(mailSubject)){ //如果找到了最新的目标邮件
                isWantMail=true; //标志位置1
            }
            
        }
        tmp = sc.nextLine(); //读取下一行邮件头
    }


    // 目标邮件,读正文
    if (isWantMail) {
        dataString = "";
        tmp = sc.nextLine();
        while (!tmp.equals(".")) {
            dataString += tmp + "\n\r";
            tmp = sc.nextLine();
        }
        String mailContent = parseMailContent(dataString);

        return mailContent; //读到了目标邮件的内容,直接返回
    } else
        while (!tmp.equals(".")) {
            tmp = sc.nextLine();
        } // 普通邮件,跳过


}
//读完了全部邮件的头部,没有找到目标邮件
writer.write("QUIT\r\n");
writer.flush(); // 结束通信
return "";

遍历的过程里发现邮件主题有些是这样的

Subject: =?UTF-8?B?572R5piT6YKu566x55So5oi36LCD56CU?=

有些还有好几行,这些是base64编码,加了头和尾的标识,那我们需要掐头去尾,再解码即可。

 //解析邮件主题
private static String parseMailSubject(String subject){
    byte[] tmp;
    if(subject.indexOf(" =?UTF-8?B?")==0){
        subject=subject.replace("?=\n\r", "").replace(" =?UTF-8?B?", "");
        tmp =Base64.getDecoder().decode(subject);
        return new String(tmp, StandardCharsets.UTF_8); 
    }
    return subject.substring(1,subject.length()-1);
}

邮件内容也是编码过的,用的是Quoted-Printable 编码,那我们照样解码即可

// 解析邮件正文
private static String parseMailContent(String quotedPrintableString) {
    char[] str = quotedPrintableString.replaceAll("=\n\r", "").toCharArray();
    ArrayList<Byte> Bytes = new ArrayList<>();
    char tmp;
    for (int i = 0; i < str.length; i++) {
        tmp = str[i];
        if (tmp == '=') {
            Bytes.add((byte) Integer.parseInt("" + str[i + 1] + str[i + 2], 16));
            i += 2;
        } else {
            Bytes.add((byte) tmp);
        }
    }
    byte[] bytes = new byte[Bytes.size()];
    for (int i = 0; i < bytes.length; i++) {
        bytes[i] = Bytes.get(i);
    }
    return new String(bytes, StandardCharsets.UTF_8);
}

        解码和读取都完成了,接下来就是从邮件的内容里读出我们想要的IP信息了。这个时候发现之前发送IP地址的时候发的是JSON字符串,而Java内置的库没有能解析JSON的库...其实不用慌,这个JSON又不是非用不可。

        两边调整一下发送的邮件正文和解析的方法,再润色一下代码就完成了。

全部代码

发送邮件
const nodemailer = require('nodemailer'); //记得先npm下载
const os = require('os');



const config = {
    mail: 'aaabbbccc@ddd.com', //自己的邮箱账号
    pass: 'eeeeffffgggghhhh',    //smtp授权码
    host: 'pop.iiii.com', //自己邮箱对应的smtp服务器
    port: 25,                //自己邮箱对应的smtp服务器端口
    secureConnection: false,     //是否加密通信
    mailSubject: "ip地址更新啦!",      //要发送的邮件主题,用于区分这些邮件和普通邮件
    scanTime:10, //多久扫描一次
    message:"动动小手点个赞~",
}


//获取IPv6公网地址,如果有多个,只取第一个
function getIPv6Address() {
    const networkInterfaces = Object.values(os.networkInterfaces());
    let tmp = {};
    for (let i = 0; i < networkInterfaces.length; i++) {
        for (let j = 0; j < networkInterfaces[i].length; j++) {
            tmp = networkInterfaces[i][j];
            if (tmp.family == 'IPv6' && tmp.internal == false) {
                return { public: true, ip: tmp.address }
            }
        }
    }
    return { public: false, ip: "::1" }
}


//发送邮件
function sendIPToMyMail({ host, port, secureConnection, mailSubject, mail, pass ,message}, { public, ip }) {

    //连接和登录配置
    const transporter = nodemailer.createTransport({
        host,       //smtp服务器
        port,       //不加密是25,
        secureConnection,//是否加密,
        auth: {
            user: mail,//邮箱账号
            pass: pass,//邮箱授权码,注意不是密码
        }
    });

    //邮件内容配置
    const mailOptions = {
        from: mail, //发件方账号,当然是自己的邮箱账号
        to: mail, //发给自己,所以这里还是自己
        subject: mailSubject, //邮件主题
        text: `${ getTime() }\r\n${ public }\r\n${ip}\r\n${message}\r\n` //用换行来划分有效信息
    };

    transporter.sendMail(mailOptions, (err, info) => {
        if (err) { console.log("发送失败 "+err); return; }
        console.log("发送成功 \r\n"+JSON.stringify(info));
    });//发送邮件

}

function getTime(){
    const date = new Date();
    const formattedTime = date.toLocaleString().replaceAll("/","-");
    console.log(formattedTime);
    return formattedTime;
}


function uploadSeverStart(config) {
    let nowIp = "";
    let ipMessage;

    setInterval(() => { //加个定时器

        ipMessage = getIPv6Address(); //获取IP
        if (ipMessage.ip != nowIp) {  //如果变化了
            sendIPToMyMail(config,ipMessage);  //发送邮件
            nowIp = ipMessage.ip; //更新IP
        }

    }, config.scanTime * 1000)
}

uploadSeverStart(config);

读取邮件
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Scanner;

public class tool {

    /*
     * 
     * 参数
     * name 邮箱账号
     * pass 邮箱POP3授权码
     * mailHost POP3服务器
     * mailPort POP3服务器端口,不加密是110端口
     * mailSubject 要读取ip邮件的主题
     * readLine 读取邮件正文报文内容的前几行
     */
    @SuppressWarnings("resource")
    public static String get(String name, String pass, String mailHost, int mailPort, String mailSubject, int readLine)
            throws UnknownHostException, IOException {

        // 准备通信
        Socket mySocket = new Socket(mailHost, mailPort);
        InputStreamReader reader = new InputStreamReader(mySocket.getInputStream());
        OutputStreamWriter writer = new OutputStreamWriter(mySocket.getOutputStream());
        Scanner sc = new Scanner(reader);
        String tmp;
        int tmpInt;
        String tmpSubject;
        sc.nextLine(); //连接成功固定返回 +ok pop3 ready,把它读掉,不然影响后面的判断

        // 登录
        writer.write("USER " + name + "\r\n");
        writer.flush();
        sc.nextLine();//读一行,丢弃
        writer.write("PASS " + pass + "\r\n");
        writer.flush();
        tmp = sc.nextLine();//再读一行,用变量接收
        if (tmp.indexOf("-ERR") == 0){
            throw new Error("mail login fail");
        }
           

        // 查看邮件数量
        writer.write("STAT\r\n");
        writer.flush();
        //这个时候会返回类似+ok 6 6666
        sc.next();//跳过+ok
        tmpInt=sc.nextInt();//接收6,表明有6封邮件
        sc.nextLine(); //读完剩下部分,以免影响接下来的部分

        // 遍历读取所有邮件的头部和前 n 行
        boolean isWantMail;
        String dataString;
        for (int i = tmpInt; i > 0; i--) { //从后往前遍历,这样会先遍历到比较新的邮件
            System.out.println("NO."+i);
            writer.write("TOP " + i + " " + readLine + "\r\n");
            writer.flush(); // 准备读 n 行
            isWantMail = false;
            tmp = sc.nextLine();

            // 读邮件头
            while (!tmp.isEmpty()) {
                if (tmp.indexOf("Subject: ") == 0) {
                    tmpSubject=tmp+"\r\n";
                    tmp=sc.nextLine();
                    while(!tmp.contains(":")){
                        tmpSubject+=tmp+"\r\n";
                        tmp=sc.nextLine();
                    }
                    tmpSubject=tmpSubject.substring(8);
                    System.out.println(tmpSubject);
                    tmpSubject=parseMailSubject(tmpSubject);
                    System.out.println(tmpSubject);
                    if(tmpSubject.equals(mailSubject)){ //如果找到了最新的目标邮件
                        isWantMail=true;
                    }
                    
                }
                tmp = sc.nextLine();
            }

            // 目标邮件,读正文
            if (isWantMail) {
                dataString = "";
                tmp = sc.nextLine();
                while (!tmp.equals(".")) {
                    dataString += tmp + "\r\n";
                    tmp = sc.nextLine();
                }

                String mailContent = parseMailContent(dataString);
                return mailContent;
            } else
                while (!tmp.equals(".")) {
                    tmp = sc.nextLine();
                } // 普通邮件,跳过
        }
        writer.write("QUIT\r\n");
        writer.flush(); // 结束通信
        return "";
    }

    // 解析邮件正文
    private static String parseMailContent(String quotedPrintableString) {
        char[] str = quotedPrintableString.replaceAll("=\r\n", "").toCharArray();
        ArrayList<Byte> Bytes = new ArrayList<>();
        char tmp;
        for (int i = 0; i < str.length; i++) {
            tmp = str[i];
            if (tmp == '=') {
                Bytes.add((byte) Integer.parseInt("" + str[i + 1] + str[i + 2], 16));
                i += 2;
            } else {
                Bytes.add((byte) tmp);
            }
        }

        byte[] bytes = new byte[Bytes.size()];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = Bytes.get(i);
        }
        return new String(bytes, StandardCharsets.UTF_8);
    }

    //解析邮件主题
    private static String parseMailSubject(String subject){
        byte[] tmp;
        if(subject.indexOf(" =?UTF-8?B?")==0||subject.indexOf(" =?utf-8?B?")==0){
            subject=subject.replace("?=\r\n", "").replace(" =?UTF-8?B?", "").replace(" =?utf-8?B?", "");
            tmp =Base64.getDecoder().decode(subject);
            return new String(tmp, StandardCharsets.UTF_8); 
        }
        return subject.substring(1,subject.length()-2);
    }


    
    public static void main(String[] args) {
        String name="aaabbbccc@ddd.com";
        String pass="eeeeffffgggghhhh";
        String mailHost="pop.iiii.com";
        int mailPort=110;
        String mailSubject="ip地址更新啦!";
        int readLine=10;
        try {

            String content=get(name, pass, mailHost, mailPort, mailSubject, readLine);
            // System.out.println(content);
            if(content.isEmpty()){
                //如果邮箱里没有目标邮件
                System.out.println("没有找到对应邮件");
            }else{
                    Scanner sc = new Scanner(content);
                    String time=sc.nextLine();//正文第一行,时间
                    System.out.println("时间\r\n"+time+"\r\n");
                    
                    boolean ispublic= sc.nextBoolean();//正文第二行,是否是公网IPv6
                    if(ispublic==false){
                        //找到了邮件,但新IP地址没有公网IPv6
                        System.out.println("没有获得公网IPv6");
                        sc.next(); //正文第三行,无效IP
                    }else{
                        String ip=sc.next();//正文第三行,有效IP
                        System.out.println("获取IP地址成功!\r\n"+ip+"\r\n");
                    }
                    String message=sc.next();//正文第四行,携带的消息
                    System.out.println("携带的消息\r\n"+message);
                    sc.close();
            }

        } catch (UnknownHostException e) {
            System.out.println(e);
        } catch (IOException e) {
            System.out.println(e);
        } 
    }

}

运行效果

$ java tool

时间

2024-8-30 12:13:30

获取IP地址成功!

2409:XXXX:XXXX:f090:500b:3928:8526:7311

携带的消息

动动小手点个赞~

总结

不足
  • 没有考虑加密,不够安全
  • 不同的POP服务器实现的细节可能不同,这里的Java代码只测试了163邮箱和QQ邮箱,不确定其他邮箱能不能用
  • 本来想把Java代码封装下,做成安卓应用的,但是莫名其妙报a.grey.BulimiaTGen.f,后来想想还是算了吧
  • 没有结合持久化存储,这样每次都要从POP服务器获取邮件。应该保存在文件里,能减少很多次网络请求
  • 这里读邮件的代码只考虑了纯文本的正文。如果正文里带上html或者图片,附件之类的,那么可能会有一堆分隔符,这里没有做解析和分隔。所以能用实现好的库或者模块的话,就尽量别造轮子了
  • 总之代码仅供学习参考,实际使用还是建议考虑云服务器和绑定域名之类的
展望
  • 代码虽然到获取完IP地址就结束了,但只要多加一点代码,它的作用能大很多。比如获取完IP后把IP加到电脑里的host文件里,就约等于DDNS了~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值