Mail.php 10.3 KB
<?php

namespace Lib\Mail;

use Lib\DbPool;
use Model\folderSql;
use Model\listsSql;

/**
 * 操作邮件
 * @author:dc
 * @time 2023/2/5 10:10
 * Class MailFun
 * @package Helper\Mail
 */
class Mail {

    /**
     * imap服务器连接实例
     * @var Imap
     */
    public Imap $client;

    /**
     * @var string
     */
    private string $username;

    /**
     * @var string
     */
    private string $password;

    /**
     * @var string
     */
    private string $server;

    /**
     * Mail constructor.
     * @param string $email
     * @param string $password
     * @param string $imap
     */
    public function __construct(string $email,string $password,string $imap)
    {
        $this->username = $email;
        $this->password = $password;
        $this->server = $imap;
    }

    /**
     * 登录imap服务器
     * @param bool $pass_err
     * @return int
     * @author:dc
     * @time 2023/3/14 10:03
     */
    public function login($pass_err=true):int {

        $this->client = new Imap();

        try {
            // 是否初始成功
            $this->client->login("ssl://{$this->server}:993",$this->username,$this->password);
        }catch (\Throwable $e){
            if($pass_err && $e->getCode() == 403){
                // 一天中超过 3次失败说明密码错误了
                if(redis()->incr('email_login_error:'.md5($this->username),86400) > 3){
                    // 登录失败了 ,
                    db()->update(\Model\emailSql::$table,['pwd_error'=>1],dbWhere(['email'=>$this->username]));
                }
                return -1;
            }else{
                logs($e->getMessage());
            }

            return $e->getCode() == 403 ? 0 : -1;

        }
        redis()->delete('email_login_error:'.md5($this->username));

        return 1;
    }


    /**
     * 同步文件夹
     * @param int $email_id
     * @param DbPool|null $db
     * @return mixed
     * @author:dc
     * @time 2023/2/5 10:58
     */
    public function syncFolder($email_id,$db=null){
        $db = $db ? $db : db();
        // 读取所有文件夹,未解密
        $folders    =   $this->client->getFolder();

//        $db->transaction();
        foreach ($folders as $folder){
            $pid = 0;
            $uuid = md5($email_id.$folder['folder']);

            // 处理子父文件夹
            if(str_contains($folder['folder'], '/')){
                // 子目录
                $folder['name'] = explode('/',$folder['parseFolder']);
                // 查找pid
                $pid = $db->value(folderSql::has(['uuid'=>md5($email_id.explode('/',$folder['folder'])[0])]));
                // 去掉父目录名称
                $folder['parseFolder'] = explode('/',$folder['parseFolder'])[1];
            }

            if(!$db->count(folderSql::has(['uuid'=>$uuid]))){
                try {
                    $db->insert(folderSql::$table,[
                        'email_id' => $email_id,
                        'folder' => folderAlias($folder['parseFolder']),
                        'origin_folder' => $folder['folder'],
                        'uuid'  =>  $uuid,
                        'pid'   =>  $pid
                    ],false);
                }catch (\Throwable $e){
                    // 这里就不处理失败了
                }
            }


        }
//        $db->commit();

    }


    /**
     * 同步邮件
     * @param $email_id
     * @param $folder_id
     * @param string $folder
     * @param null|DbPool $db
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2023/2/18 9:54
     */
    public function syncMail($email_id,$folder_id,$folder='INBOX',$db = null):bool {
//        _echo('正在同步文件夹:'.$folder);
        $db = $db ? $db : db();
        // 选择文件夹
        $status =   $this->client->selectFolder($folder);

        // 是否有邮件
        if (!isset($status['EXISTS']) || !$status['EXISTS']){
            return true;
        }

        // 更新数量
        $db->update(
            folderSql::$table,
            ['exsts'=>$status['EXISTS'],'unseen'=>$status['UNSEEN']??0],
            dbWhere(['id'=>$folder_id]),
            false
        );


        // 最后拉取的msgno
        $lastMsgno   =   $db->value(listsSql::lastMsgno($email_id,$folder_id));

        $nu = 20;

        if(!$lastMsgno){
            $msgno = range(1,$nu);
        }else{
            $msgno = range($lastMsgno,$lastMsgno+$nu);

            if($lastMsgno > $status['EXISTS']){
                $msgno = range($status['EXISTS'] > $nu ? $status['EXISTS'] - $nu : 1,$status['EXISTS']);
            }
            // 一样就不拉新的
            if($lastMsgno == $status['EXISTS']){
                return true;
            }
        }


        // 循环
        $results = $this->client->fetchHeader($msgno);
        if($results){
            // 表示已存在新邮件
            redis()->incr('have_new_mail_'.$email_id,120);

            // 批量插入
            foreach ($results as $key=>$result){
                $header = $result['HEADER.FIELDS'];

                foreach ($result['FLAGS'] as $k=>$FLAG){
                    $result['FLAGS'][$k] = strtolower(str_replace('\\','',$FLAG));
                }
                try {

                    $file_header = $result['BODYSTRUCTURE'];

                    // 没有收件人
                    if(!empty($header['To'])){
                        $header['To'] = MailFun::toOrFrom($header['To']);
                    }else{
                        $header['To'] = [];
                    }

                    $header['From'] = MailFun::toOrFrom($header['From']);

                    $data   =   [
                        'msgno'   =>  $key,
                        'uid'   =>  $result['UID'],
                        'subject'   =>  $header['Subject'],
                        'cc'    =>  $header['Cc']??'',
                        'bcc'    =>  $header['Bcc']??'',
                        'from'   =>  $header['From'][0]['email']??'',
                        'from_name'   =>  $header['From'][0]['name']??'',
                        'to'   =>  $header['To']?implode(',',array_column($header['To'],'email')):'',
                        'to_name'   =>  json_encode($header['To']),
                        'date'   =>  isset($header['Date'])&&$header['Date'] ? strtotime(is_array($header['Date']) ? $header['Date'][0] : $header['Date']) : strtotime($result['INTERNALDATE']),
                        'message_id'   =>  $header['Message-ID']??'',
                        'udate'   =>  strtotime($result['INTERNALDATE']),
                        'size'   =>  $result['RFC822.SIZE']??0,
                        'recent'   =>  in_array('recent',$result['FLAGS']),
                        'seen'   =>  in_array('seen',$result['FLAGS']),
                        'draft'   =>  in_array('draft',$result['FLAGS']),
                        'flagged'   =>  in_array('flagged',$result['FLAGS']),
                        'answered'   =>  in_array('answered',$result['FLAGS']),
                        'folder_id'   =>  $folder_id,
                        'email_id'    =>  $email_id,
                        'uuid'  =>  md5($email_id.$folder_id.$result['UID']),
                        'is_file'  =>  MailFun::isFile($file_header[$key]['BODYSTRUCTURE']??[]) //是否附件
                    ];
                }catch (\Throwable $e){
                    logs(
                        '邮件解析失败:'.PHP_EOL.$e->getMessage().PHP_EOL.print_r($result,true),
                        LOG_PATH.'/imap/mail/'.$email_id.'/'.$result['UID'].'.log'
                    );
                    unset($results[$key]);
                    continue;
                }

                $results[$key]  =   $data;
            }

            // 保存数据,这里其实不用再次写循环的。我想写一个
            $uuids = $db->all(listsSql::hasUuid(array_column($results,'uuid')));
            $uuids = $uuids ? array_column($uuids,null,'uuid') : [];

//            $db->transaction();
            foreach ($results as $insert){
                if(empty($uuids[$insert['uuid']])){
                    // 新增
                    $db->insert(listsSql::$table,$insert);
                }else{
                    // 修改
                    $db->update(
                        listsSql::$table,
                        $insert,
                        dbWhere(['id'=>$uuids[$insert['uuid']]['id']])
                    );
                }
            }
//            $db->commit();

            // 更新数量
            $db->update(
                folderSql::$table,
                ['last_sync_time' => time()],
                dbWhere(['id'=>$folder_id]),
                false
            );
            // 结束操作了

            // 再次调用
            $this->syncMail($email_id,$folder_id,$folder,$db);
        }

        return true;

    }


    /**
     * 同步 邮件 内容 body
     * @param $id
     * @param $msgno
     * @param $email_id
     * @param $folder_name
     * @param $email
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2023/2/9 10:29
     */
    public static function syncBody($id,$msgno, $email_id,$folder_name,$email):bool {

        // 选择文件夹
        static::$client[$email]->selectFolder($folder_name);

        $body = static::$client[$email]->fetchBody([$msgno],storage_path('email/'.$email_id));

        if(!empty($body[$msgno]['RFC822.TEXT'])){
            \App\Models\Body::_insert($id,$body[$msgno]['RFC822.TEXT']);
        }

        return true;

    }


    /**
     * 设置为未读
     * @param $uids
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/10/26 17:11
     */
    public function seen($uids,$folder,$seen):bool{
        // 选择目录
        $status =   $this->client->selectFolder($folder);

        return $this->client->flags($uids,[Imap::FLAGS_SEEN],$seen ? '+' : '-',true);
    }

    /**
     * 移动邮件,
     * @param $uids
     * @param $folder
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2023/3/21 14:54
     */
    public function move($uids,$folder,$to_folder){
        // 选择目录
        $status =   $this->client->selectFolder($folder);

        return $this->client->folderRename($uids,[Imap::FLAGS_DELETED],$recycle ? '+' : '-',true);

    }





}