SyncMail.php 10.6 KB
<?php

namespace Service;

use Event\Event;
use Lib\Imap\ImapConfig;
use Lib\Imap\ImapPool;
use Lib\Imap\Parse\Folder\Folder;
use Lib\Imap\Parse\MessageItem;
use Model\bodySql;
use Model\emailSql;
use Model\folderSql;
use Model\listsSql;

/**
 * 同步邮件
 * @author:dc
 * @time 2024/9/26 9:31
 * Class SyncMail
 * @package Service
 */
class SyncMail {

    /**
     * @var \Lib\Db|\Lib\DbPool
     */
    protected $db;

    /**
     * @var \Lib\Imap\Imap
     */
    protected $imap;

    /**
     * @var array
     */
    protected $email;


    protected $isStop = false;

    /**
     * SyncMail constructor.
     * @param int|string|array $email
     * @throws \Exception
     */
    public function __construct(int|string|array $email)
    {
        $this->db = db();

        if(!is_array($email)){
            $email = $this->db->first(emailSql::first($email));
            if(!$email){
                abort('未查询到邮箱');
            }
        }
        $this->email = $email;
        // 实例一个imap类
        $this->imap = ImapPool::get(
            (new ImapConfig())
                ->setHost($email['imap'])
                ->setEmail($email['email'])
                ->setPassword(base64_decode($email['password']))
//                ->debug()
        );

        $this->login();

    }

    public function stop(){
        $this->isStop = true;
    }


    protected function emailId(){
        return $this->email['id'];
    }

    /**
     * 登录imap
     * @throws \Exception
     * @author:dc
     * @time 2024/9/26 9:58
     */
    private function login(){
        $login = $this->imap->login();
        if(!$login->isOk()){
            abort($login->getMessage()?:'连接服务器异常');
        }
    }

    /**
     * @param $folder
     * @param int $pid
     * @return array
     * @throws \Exception
     * @author:dc
     * @time 2024/9/26 10:46
     */
    protected function folder($folder,$pid = 0){
        $uuids = [];

        foreach ($folder as $item){
            /** @var Folder $item*/

            $uuid = md5($this->emailId().$item->folder);
            $uuids[$uuid] = $uuid;

            $folder_name = '';
            if($item->flags){
                // 有些邮箱是把公共的文件夹标记在flag里面的,识别出来
                foreach ($item->flags as $flag){
                    if(in_array($flag,['Send','Drafts','Junk','Trash'])){
                        $folder_name = folderAlias($flag);
                    }
                }
            }
            if(!$folder_name){
                $fn = explode('/',$item->getParseFolder());
                $folder_name = folderAlias(end($fn));
            }

            // 是否存在
            $id = $this->db->value(folderSql::has(['email_id'=>$this->emailId(),'uuid'=>$uuid]));
            $data = [
                'email_id' => $this->emailId(),
                'folder' => $folder_name,
                'origin_folder' => $item->folder,
                'uuid'  =>  $uuid,
                'pid'   =>  $pid
            ];
            if ($id){
                $this->db->update(folderSql::$table,$data,dbWhere(['id'=>$id]),false);
            }else{
                $id = $this->db->insert(folderSql::$table,$data,false);
                if(!$id) abort('文件夹写入异常 '.json_encode($data,JSON_UNESCAPED_UNICODE));
            }
            // 是否有子级目录
            if($id && $item->getChild()){
                $uuids = array_merge($uuids,$this->folder($item->getChild(),$id));
            }
        }

        return $uuids;
    }

    /**
     * 同步
     * @throws \Exception
     * @author:dc
     * @time 2024/9/26 10:46
     */
    public function sync(){
        $this->isStop = false;
        /*********************************** 同步文件夹 ***************************************/
        // 获取文件夹
        $folders = $this->imap->getFolders();
        $uuids = $this->folder($folders->getTopFolder());
        if($uuids){
            // 删除以前的
            $this->db->delete(folderSql::$table,['uuid.notin'=>$uuids,'email_id'=>$this->emailId()]);
        }

        if($this->isStop) return;


        /********************* 同步邮件 **********************/

        // 循环文件夹
        foreach ($folders->all() as $f){
            if($this->isStop) return;

            if($f->isSelect){ // 是否可以选择 只有可以选中的文件夹才有邮件
                $folder = $this->imap->folder($f); // 选择文件夹后,有状态

                // 这个暂时不要
//                $this->db->update(folderSql::$table,[
//                   'exsts' =>   $folder->getTotal(),
//                   'unseen' =>   $folder->getUnseen(),
//                   'last_sync_time' => time()
//                ],dbWhere(['email_id'=>$this->emailId(),'origin_folder'=>$folder->getName()]),false);

                // 是否有邮件 有邮件才继续
                if ($folder->getTotal()){
                    $this->mail($folder);
                }
            }
        }


    }

    /**同步邮件
     *
     * @param string|\Lib\Imap\Request\Folder $folder
     * @param array $uids 固定的uid
     * @param false $isBody 是否同时同步body
     * @author:dc
     * @time 2024/9/26 11:10
     */
    public function mail(string|\Lib\Imap\Request\Folder $folder, array $uids = [],$isBody = false){
        if(is_string($folder)){
            $folder = $this->imap->folder($folder);
        }

        $folder_id = $this->db->value(folderSql::first([
            'email_id'=>$this->emailId(),
            'uuid'  =>  md5($this->emailId().$folder->getName())
        ],'`id`'));

        // 选择成功
        if($folder->isOk()){
            $msg = $folder->msg();
            if($uids){
                $this->saveMail($folder_id,$msg->uid($uids)->get()->all(),$isBody);
            }else{
                $p=1;
                while (1){
                    if($this->isStop) return;

                    $lists = $msg->forPage($p)->get()->all();
                    // 没有数据就跳出
                    if(!$lists){
                        break;
                    }else{
                        $p++;
                        $this->saveMail($folder_id,$lists,$isBody);
                    }
                }
            }

        }

    }

    /**
     * 保存邮件列表
     * @param int $folder_id
     * @param MessageItem[] $lists
     * @param bool $isBody
     * @author:dc
     * @time 2024/9/29 15:14
     */
    protected function saveMail(int $folder_id, array $lists, bool $isBody=false){
        foreach ($lists as $item){

            $data   =   [
                'uid'   =>  $item->uid,
                'subject'   =>  mb_substr($item->header->getSubject(),0,1000),// 控制下,有的蛋疼,整tm多长
                'cc'    =>  $item->header->getCc(true),
                'bcc'    =>  $item->header->getBcc(true),
                'from'   =>  $item->header->getFrom()->email,
                'from_name'   =>  $item->header->getFrom()->name,
                'to'   =>  implode(',',array_column($item->header->getTo(true),'email')),
                'to_name'   =>  $item->header->getTo(true),
                // 这个是 邮件的时间 就是header里面带的 一般情况就是发件时间
//                'date'   =>  strtotime($item->header->getDate()),
                'udate'   =>  strtotime($item->date), // 有这个时间就够了,内部时间,就是收到邮件的时间
                'size'   =>  $item->size,
                'recent'   =>  $item->isRecent() ? 1 : 0,
                'seen'   =>  $item->isSeen() ? 1 : 0,
                'draft'   =>  $item->isDraft() ? 1 : 0,
                'flagged'   =>  $item->isFlagged() ? 1 : 0,
                'answered'   =>  $item->isAnswered() ? 1 : 0,
                'folder_id'   =>  $folder_id,
                'email_id'    =>  $this->emailId(),
                'is_file'  =>  $item->isAttachment() ? 1: 0 //是否附件
            ];

            // 查询是否存在
            $id = $this->db->value(listsSql::first(dbWhere([
                'email_id'=> $data['email_id'],
                'folder_id' =>  $data['folder_id'],
                'uid'   =>  $data['uid']
            ]),'`id`'));


            if(!$id){

                $id = $this->insert($data);
                if(!$id){
                    continue;
                }
                // 新邮件标记
                if($item->getFolderName() == 'INBOX')
                    redis()->incr('have_new_mail_'.$this->emailId(),120);
                // 执行事件
                $data['Aicc-Hot-Mail'] = $item->header->get('Aicc-Hot-Mail');

                Event::call('mail_sync_list',$id, $data);

            }else{
                $this->db->update(listsSql::$table,$data,dbWhere(['id'=> $id]));
            }


            // 是否同步body内容
            if($isBody){

                $body = [
                    'lists_id'  =>  $id,
                    'text_html' =>  []
                ];

                foreach ($item->getBody()->getItems() as $item){
                    $body['text_html'] = [
                        'body'  =>  $item->body,
                        'content-type'  =>  $item->get('content-type'),
                        'content-id'  =>  $item->get('content-id'),
                        'charset'   =>  $item->get('charset')
                    ];
                }

                if($this->db->count(bodySql::has($id))){
                    $this->db->update(bodySql::$table,$body,'`lists_id` = '.$id,false);
                }else{
                    $this->db->insert(bodySql::$table,$body,false);
                }

            }


        }


    }

    /**
     * 查询数据 并重试
     * @param array $data
     * @param int $num
     * @return int
     * @author:dc
     * @time 2024/10/12 15:32
     */
    protected function insert(array $data, int $num = 0){
        if($num>2){
            return 0;
        }
        try {
            $id = $this->db->throw()->insert(listsSql::$table,$data);
        }catch (\Throwable $e){
            // 字符串编码异常
            if(stripos($e->getMessage(),'SQLSTATE[HY000]: General error: 1366 Incorrect string value:')!==false){
                // 编码异常的 字段
                preg_match("/for column '([a-z0-9_]{2,})' at/",$e->getMessage(),$filed);
                if(!empty($filed[1]) && isset($data[$filed[1]])){
                    // 进行编码转换 大概率会失败
                    $data[$filed[1]] = mb_convert_encoding($data[$filed[1]],'UTF-8');
                }

                $id = $this->insert($data,$num+1);

            }
            logs($e->getMessage());
        }

        return $id;
    }













}