<?php namespace Lib\Mail; use Event\syncMail; use Lib\DbPool; use Model\bodySql; 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; $this->client = new Imap(); } /** * 登录imap服务器 * @param bool $pass_err * @return int * @author:dc * @time 2023/3/14 10:03 */ public function login($pass_err=true):int { // 处理url $host = MailFun::getHostPort($this->server,993,'ssl://'); try { // 是否初始成功 $this->client->login($host['host'].':'.$host['port'],$this->username,$this->password); }catch (\Throwable $e){ logs($this->username.'===>'.$e->getMessage()); if($pass_err){ // 是否是密码错误 foreach ([ 'NO [ALERT] Invalid credentials (Failure)',// 登录失败 'NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)',// 登录失败 'NO [AUTHENTICATIONFAILED] Authentication failed.',// 登录失败 权限 'NO LOGIN Login error',// 登录失败 'NO LOGIN auth error',// 登录失败 'NO ERR.LOGIN.PASSERR',// 登录失败 密码错误 'NO Login fail.',// 登录失败 'NO LOGIN failed.', // 登录失败 // 'NO ERR.LOGIN.REQCODE', // 未知错误 'NO [ALERT] Application-specific password', // 这个错误是没有提供特定的授权码 'NO LOGIN Login error, user name or password error' ] as $em){ if(str_contains($e->getMessage(), $em)){ db()->update( \Model\emailSql::$table, ['pwd_error'=>1], dbWhere(['email'=>$this->username]) ); } } // 一天中超过 3次失败说明密码错误了 // if(redis()->incr('email_login_error:'.md5($this->username),86400) > 10){ // 登录失败了 , // db()->update(\Model\emailSql::$table,['pwd_error'=>1],dbWhere(['email'=>$this->username])); // } return -1; } 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(); foreach ($folders as $k=>$item){ $pname = explode('/',$item['folder']); if(count($pname)>1){ array_pop($pname); $pname = implode('/',$pname); }else{ $pname = ''; } $folders[$k]['pname'] = $pname; } $p = 0; $uuids = []; while ($folders){ foreach ($folders as $fk=>$folder){ $uuid = md5($email_id.$folder['folder']); $uuids[$uuid] = $uuid; // 查找/出现的次数 if (substr_count($folder['folder'],'/') == $p){ // 查找pid $pid = $db->value(folderSql::has(['uuid'=>md5($email_id.$folder['pname'])])); $pid = $pid ? $pid : 0; // try { $folder_name = ''; // 已发送 if(in_array('Send',$folder['check'])){ $folder_name = folderAlias('Send'); } // 草稿 elseif(in_array('Drafts',$folder['check'])){ $folder_name = folderAlias('Drafts'); } // 垃圾 elseif(in_array('Junk',$folder['check'])){ $folder_name = folderAlias('Junk'); } // 回收站 elseif(in_array('Trash',$folder['check'])){ $folder_name = folderAlias('Trash'); } if(!$folder_name){ $fn = explode('/',$folder['parseFolder']); $folder_name = folderAlias(end($fn)); } if(!$db->count(folderSql::has(['uuid'=>$uuid]))){ $db->insert(folderSql::$table,[ 'email_id' => $email_id, 'folder' => folderAlias($folder_name), 'origin_folder' => $folder['folder'], 'uuid' => $uuid, 'pid' => $pid ],false); }else{ $db->update(folderSql::$table,[ 'email_id' => $email_id, 'folder' => folderAlias($folder_name), 'origin_folder' => $folder['folder'], 'uuid' => $uuid, 'pid' => $pid ],dbWhere(['email_id' => $email_id,'uuid' => $uuid]),false); } // }catch (\Throwable $e){ // 这里就不处理失败了 // } unset($folders[$fk]); } } $p++; } if($uuids){ // 删除以前的 $db->delete(folderSql::$table,['uuid.notin'=>$uuids,'email_id'=>$email_id]); } } /** * 同步邮件 * @param $email_id * @param $folder_id * @param string $folder * @param null|DbPool $db * @return bool|array * @throws \Exception * @author:dc * @time 2023/2/18 9:54 */ public function syncMail($email_id,$folder_id,$folder='INBOX') { if(empty($folder)){ return 0; } // _echo('正在同步文件夹:'.$folder); $db = db(); // 选择文件夹 try { $status = $this->client->selectFolder($folder); }catch (\Throwable $e){ return 0; } // 是否有邮件 if (!is_array($status) || !isset($status['EXISTS']) || !$status['EXISTS']){ return true; } // 更新数量 $upFolderData = ['exsts'=>$status['EXISTS'],'last_sync_time' => time()]; // 谷歌 不返未读数量 谢特 if(isset($status['UNSEEN'])){ $upFolderData['unseen'] = $status['UNSEEN']; } $db->update( folderSql::$table, $upFolderData, dbWhere(['id'=>$folder_id]), false ); // 读取黑名单 $blacklist = redis()->get('blacklist:'.$email_id); $blackFolder = ''; if($blacklist){ $blackFolder = $db->value(folderSql::originFolder($email_id,'垃圾箱')); } // $nu = 100; $msgno = 1; $success_uid = []; while (true){ // 结束操作了 if(redis()->get(SYNC_RUNNING_REDIS_KEY) == 'stop'){ break; } // 是否结束了 if($status['EXISTS'] < $msgno){ break; } // 是否超过了最大数量 $maxmsgno = ($msgno-1)+$nu; if($maxmsgno > $status['EXISTS']){ $maxmsgno = $status['EXISTS']; } $uids = $this->client->fetch(range($msgno,$maxmsgno),'UID'); if(!$uids){ break; } $uids = array_column($uids,'UID'); $existsUids = $db->all(listsSql::getUids($email_id,$folder_id,$uids)); if($existsUids){ $existsUids = array_column($existsUids,'uid'); // 获取不存在数据库的uid $uids = array_diff($uids,$existsUids); } $msgno += $nu; // 开始同步 if($uids){ $this->syncUidEmail( $uids, $email_id, $folder, $folder_id, $blacklist, $blackFolder, $db ); $success_uid = array_merge($success_uid,$uids); } } // 更新数量 if(!isset($status['UNSEEN'])){ // 统计未读数量 $unseen = $db->count(listsSql::listCount(dbWhere([ 'seen' => 0, 'deleted' => 0, 'email_id' => $email_id, 'folder_id' => $folder_id, ]))); $db->update( folderSql::$table, ['unseen' => $unseen], dbWhere(['id'=>$folder_id]), false ); } return $success_uid; } /** * 同步邮件 只通过 uid获取 * @param array $uids * @param $email_id * @param $folder * @param $folder_id * @param $blacklist * @param $blackFolder * @param \Lib\DbPool $db * @throws \Exception * @author:dc * @time 2023/8/2 15:35 */ public function syncUidEmail(array $uids,$email_id,$folder,$folder_id,$blacklist,$blackFolder,$db){ $results = $this->client->fetchHeader($uids,true); if($results && is_array($results)){ // 表示已存在新邮件 if($folder == 'INBOX') 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 { foreach ($header as $k=>$item){ $header[strtolower($k)] = $item; } // 没有收件人 $header['to'] = MailFun::toOrFrom($header['to']??''); $header['from'] = MailFun::toOrFrom($header['from']); // 抄送 ,密送 $cc = []; $bcc = []; if($header['cc']??''){ $cc = MailFun::toOrFrom($header['cc']); } if($header['bcc']??''){ $bcc = MailFun::toOrFrom($header['bcc']); } $data = [ 'uid' => $result['UID'], 'subject' => $header['subject']??($header['Subject']??($header['SUBJECT']??'')), 'cc' => $cc, 'bcc' => $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' => strtotime(is_array($header['date']??'') ? $header['date'][0] : $header['date']??''), // 'message_id' => $header['message-id']??'', 'udate' => strtotime($result['INTERNALDATE']), 'size' => $result['RFC822.SIZE']??0, 'recent' => in_array('recent',$result['FLAGS']) ? 1 : 0, 'seen' => in_array('seen',$result['FLAGS']) ? 1 : 0, 'draft' => in_array('draft',$result['FLAGS']) ? 1 : 0, 'flagged' => in_array('flagged',$result['FLAGS']) ? 1 : 0, 'answered' => in_array('answered',$result['FLAGS']) ? 1 : 0, 'folder_id' => $folder_id, 'email_id' => $email_id, 'is_file' => MailFun::isFile($result['BODYSTRUCTURE']??'') ? 1: 0 //是否附件 ]; $data['date'] = $data['date'] ? : 0; // 验证是否存在黑名单中 if($blacklist && $blackFolder!=$folder){ // 邮箱是否在黑名单中 $isBlacklist = false; if (!empty($blacklist['emails']) && is_array($blacklist['emails']) && in_array($data['from'],$blacklist['emails'])){ $isBlacklist = true; } // 域是否存在 if (!empty($blacklist['domain']) && is_array($blacklist['domain']) && in_array(explode('@',$data['from'])[1],$blacklist['domain'])){ $isBlacklist = true; } if($isBlacklist && $blackFolder){ // 移入垃圾箱 try { $this->client->move([$result['UID']],$blackFolder); }catch (\Throwable $e){ logs('移动邮件失败 '.$result['UID'].':'.$e->getMessage().$e->getTraceAsString()); } continue; } } }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; } // 插入数据库 // 主题太长了就截取掉 $data['subject'] = mb_substr($data['subject'],0,3500); try { $id = $db->throw()->insert(listsSql::$table,$data); if($id){ try { go(function ($id,$header,$data){ new syncMail($id,$header,$data); },...[$id,$header,$data]); }catch (\Throwable $e){ logs($e->getMessage()); } } }catch (\Throwable $e){ // 插入失败,尝试更新 $db->update(listsSql::$table,$data,dbWhere([ 'email_id'=> $data['email_id'], 'folder_id' => $data['folder_id'], 'uid' => $data['uid'] ])); } $results[$key] = []; } } } /** * 同步 邮件 内容 body * @param $folder_name * @param $uid * @param $id * @param null $db * @return bool * @throws \Exception * @author:dc * @time 2023/4/23 17:40 */ public function syncBody($folder_name, $uid , $id, $db=null):bool { if(empty($folder_name)){ return 0; } $db = $db ? $db : db(); // 选择文件夹 $this->client->selectFolder($folder_name); $body = $this->client->fetchBody([$uid],MAIL_ATTACHMENT_PATH,true); $body = array_values($body); $body = $body[0]['RFC822.TEXT']??''; if(!empty($body)){ $description = ''; foreach ($body as $key=>$item){ if(!empty($item['body'])){ // 过滤二进制 $item['body'] = preg_replace('/<0x[a-f\d]+>/','',$item['body']); $body[$key]['body'] = base64_encode($item['body']); } if(!$description && in_array($item['type']??'',['text/html','text/plain'])){ if(!empty($item['charset'])){ $value = @iconv($item['charset'],'utf-8',$item['body']); $value = $value ? $value : $item['body']; }else{ $value = $item['body']; } $value = @html_entity_decode($value, ENT_COMPAT, 'UTF-8'); $value=preg_replace("/<(script.*?)>(.*?)<(\/script.*?)>/si","",$value); //过滤script标签 $value=preg_replace("/<(\/?script.*?)>/si","",$value); //过滤script标签 $value=preg_replace("/javascript/si","Javascript",$value); //过滤script标签 $value=preg_replace("/<(style.*?)>(.*?)<(\/style.*?)>/si","",$value); //过滤style标签 $value=preg_replace("/<(\/?style.*?)>/si","",$value); //过滤style标签 $value = strip_tags($value); $value = str_replace(["\n","\\n"," "],'',$value); $description = mb_substr(trim($value),0,190); } if(!empty($body[$key]['filename'])){ $body[$key]['filename'] = base64_encode($body[$key]['filename']); } if(!empty($body[$key]['name'])){ $body[$key]['name'] = base64_encode($body[$key]['name']); } } bodySql::insertOrUpdate([ 'lists_id' => $id, 'text_html' => $body // todo::因为邮件会出现多编码问题,会导致数据库写不进去 ]); // 更新描述 try { $db->update(listsSql::$table,[ 'description' => @base64_encode($description) ? $description : '', 'is_file' => MailFun::isBodyFile($body) ],dbWhere([ 'id' => $id ])); }catch (\Throwable $e){ $db->update(listsSql::$table,[ 'is_file' => MailFun::isBodyFile($body) ],dbWhere([ 'id' => $id ])); } } 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 * @param $del * @return bool * @throws \Exception * @author:dc * @time 2024/3/9 16:50 */ public function deleted($uids,$folder,$del=true):bool{ // 选择目录 $status = $this->client->selectFolder($folder); return $this->client->flags($uids,[Imap::FLAGS_DELETED],$del ? '+' : '-',true); } /** * 回复标记 * @param $uids * @param $folder * @param $seen * @return bool * @throws \Exception * @author:dc * @time 2023/4/6 17:10 */ public function answered($uids,$folder,$seen):bool{ // 选择目录 $status = $this->client->selectFolder($folder); return $this->client->flags($uids,[Imap::FLAGS_ANSWERED],$seen ? '+' : '-',true); } /** * 回复标记 * @param $uids * @param $folder * @param $flagged * @return bool * @throws \Exception * @author:dc * @time 2023/4/6 17:10 */ public function flagged($uids,$folder,$flagged):bool{ // 选择目录 $status = $this->client->selectFolder($folder); return $this->client->flags($uids,[Imap::FLAGS_FLAGGED],$flagged ? '+' : '-',true); } /** * 复制 * @param $uids * @param $folder * @param $to_folder * @return bool * @throws \Exception * @author:dc * @time 2023/3/22 16:38 */ public function copy($uids,$folder,$to_folder){ // 选择目录 $status = $this->client->selectFolder($folder); return $this->client->copy($uids,$to_folder); } /** * 移动邮件 * @param $uids * @param $folder * @param $to_folder * @return bool * @throws \Exception * @author:dc * @time 2023/3/22 18:06 */ public function move($uids,$folder,$to_folder){ // 选择目录 $status = $this->client->selectFolder($folder); return $this->client->move($uids,$to_folder); } /** * 清空标记为已删除的邮件,不可还原邮件 * @author:dc * @time 2024/3/14 14:11 */ public function expunge(){ return $this->client->expunge(); } // /** // * 删除 // * @param $uids // * @param $folder // * @return bool // * @throws \Exception // * @author:dc // * @time 2023/3/22 17:52 // */ // public function delete($uids,$folder){ // // 选择目录 // $status = $this->client->selectFolder($folder); // // return $this->client->delete($uids); // } }