<?php namespace Service; use Event\Event; use Lib\Imap\Fun; use Lib\Imap\Imap; 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,\Lib\Imap\Imap|null $imap = null) { $this->db = db(); if(!is_array($email)){ $email = $this->db->cache(3600)->first(emailSql::first($email)); if(!$email){ abort('未查询到邮箱'); } } $this->email = $email; // 实例一个imap类 if($imap instanceof \Lib\Imap\Imap){ $this->imap = $imap; }else{ $this->imap = new Imap( (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()){ foreach ([ '[ALERT] Invalid credentials (Failure)',// 登录失败 '[AUTHENTICATIONFAILED] Invalid credentials (Failure)',// 登录失败 '[AUTHENTICATIONFAILED] Authentication failed.',// 登录失败 权限 'LOGIN Login error',// 登录失败 'LOGIN auth error',// 登录失败 'ERR.LOGIN.PASSERR',// 登录失败 密码错误 'Login fail.',// 登录失败 'LOGIN failed.', // 登录失败 // 'NO ERR.LOGIN.REQCODE', // 未知错误 '[ALERT] Application-specific password', // 这个错误是没有提供特定的授权码 'LOGIN Login error, user name or password error' ] as $em){ if(str_contains($login->getMessage(), $em)){ $this->db->update( \Model\emailSql::$table, ['pwd_error'=>1], dbWhere(['id'=> $this->emailId()]) ); } } 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; } /** * @param bool $syncMail * @return bool|void * @throws \Exception * @author:dc * @time 2024/10/18 17:53 */ public function sync($syncMail = true){ $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 (!$syncMail) return true; _echo($this->emailId().' ===> 文件夹同步成功'); if($this->isStop) return; /********************* 同步邮件 **********************/ // 循环文件夹 foreach ($folders->all() as $f){ if($this->isStop) return; if($f->isSelect){ // 是否可以选择 只有可以选中的文件夹才有邮件 $folder = $this->imap->folder($f); // 选择文件夹后,有状态 // 是否有邮件 有邮件才继续 if ($folder->getTotal()){ $num = $this->mail($folder); if($num){ _echo($this->emailId().' ===> '.$folder->getName().' ===> '.$num); } } // 更新数量 // $this->db->update(folderSql::$table,[ // 'exsts' => $this->db->count(listsSql::listCount( // dbWhere( // [ // 'folder_id'=>$this->getFolderId($folder->getName()), // 'deleted' => 0, // ] // ) // )), // 'unseen' => $this->db->count(listsSql::listCount( // dbWhere( // [ // 'folder_id'=>$this->getFolderId($folder->getName()), // 'seen' => 0, // 'deleted' => 0, // ] // ) // )), // 'last_sync_time' => time() // ],dbWhere(['email_id'=>$this->emailId(),'uuid'=>md5($this->emailId().$folder->getName())]),false); } } } /** * 当前 目录的id * @param string $name * @return mixed|null * @author:dc * @time 2024/10/12 17:44 */ private function getFolderId(string $name){ return $this->db->cache(120)->value(folderSql::first([ 'email_id'=>$this->emailId(), 'uuid' => md5($this->emailId().$name) ],'`id`')); } /**同步邮件 * * @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):int { $sync_number = 0; if(is_string($folder)){ $folder = $this->imap->folder($folder)->exec(); } $folder_id = $this->getFolderId($folder->getName()); // 选择成功 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 $sync_number; $uids = $msg->forPage($p)->getUids(); if($uids){ $p++; foreach ($uids as $k=>$uid){ if($this->db->cache(86400*30,false)->value(listsSql::first(dbWhere(['email_id'=>$this->emailId(),'folder_id'=>$folder_id,'uid'=>$uid]),'count(*) as c'))){ unset($uids[$k]); } } if(!$uids) continue; $lists = $msg->uid($uids)->get()->all(); $sync_number += count($lists); // 没有数据就跳出 if($lists){ $this->saveMail($folder_id,$lists,$isBody); } }else{ break; } } } } return $sync_number; } /** * 保存邮件列表 * @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 //是否附件 ]; $data['from'] = mb_substr($data['from'],0,120); // 不知道为什么 有些邮件标题有下划线,但是发件那边并没有添加下划线 $data['subject'] = str_replace('_',' ',$data['subject']); // 查询是否存在 $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 && $item->body->getRaw()){ $body = [ 'lists_id' => $id, 'text_html' => [] ]; $body['text_html'][] = [ 'body' => base64_encode($item->getBody()->getHtml() ? : $item->getBody()->getText()), 'type' => 'text/html', 'charset' => 'utf-8', 'encode' => 'base64', ]; // 处理附件 foreach ($item->getBody()->getAttachment() as $itemBody){ $tmp = [ 'body' => '', 'type' => $itemBody->getFileType(), 'charset' => 'binary', 'encode' => $itemBody->data->get('content-transfer-encoding'), 'name' => $itemBody->getFilename(), 'filename' => $itemBody->getFilename(), 'path' => $itemBody->save(MAIL_ATTACHMENT_PATH) ]; if(!$tmp){ throw new \Exception('请检查附件是否有写入权限'); } if($itemBody->getContentId()){ $tmp['content-id'] = $itemBody->getContentId(); } if($itemBody->data->get('Content-Disposition')){ $tmp['content-disposition'] = $itemBody->data->get('Content-Disposition'); } $tmp['signName'] = explode('/',$tmp['path']); $tmp['signName'] = end($tmp['signName']); $body['text_html'][] = $tmp; } 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); } // 更新描述 $this->db->update(listsSql::$table,['description'=>mb_substr($item->getBody()->getText(),0,150)],dbWhere(['id'=> $id])); } } } /** * 查询数据 并重试 * @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(),'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]] = Fun::mb_convert_encoding($data[$filed[1]],'UTF-8'); } $id = $this->insert($data,$num+1); } logs([$data,$e->getMessage()]); } return $id??0; } public function __destruct() { // TODO: Implement __destruct() method. ImapPool::release($this->imap); unset($this->imap); } }