<?php namespace Lib\Mail; 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; } /** * 登录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 { if(empty($folder)){ return 0; } // _echo('正在同步文件夹:'.$folder); $db = $db ? $db : db(); // 选择文件夹 $status = $this->client->selectFolder($folder); // 是否有邮件 if (!is_array($status) || !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 && 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 { // 没有收件人 if(!empty($header['To'])){ $header['To'] = MailFun::toOrFrom($header['To']); }else{ $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 = [ 'msgno' => $key, 'uid' => $result['UID'], '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, 'uuid' => md5($email_id.$folder_id.$result['UID']), 'is_file' => MailFun::isFile($result['BODYSTRUCTURE']??'') ? 1: 0 //是否附件 ]; $data['date'] = $data['date'] ? : 0; }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']])){ // 新增 try { $id = $db->insert(listsSql::$table,$insert); // 同步body内容 redis()->rPush('sync_email_body', [ 'lists_id' => $id, 'email_id' => $email_id, 'folder_id' => $folder_id, 'folder' => $folder, 'uid' => $insert['uid'], ]); }catch (\Throwable $e){ } }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 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(!$description && in_array($item['type']??'',['text/html','text/plain'])){ if(!empty($item['charset'])){ $value = iconv($item['charset'],'utf-8',$item['body']); }else{ $value = $item['body']; } $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]['body'])){ $body[$key]['body'] = base64_encode($body[$key]['body']); } 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($db,[ 'lists_id' => $id, 'text_html' => $body // todo::因为邮件会出现多编码问题,会导致数据库写不进去 ]); // 更新描述 $db->update(listsSql::$table,[ 'description' => $description ],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 $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 $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); } // /** // * 删除 // * @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); // } }