Dans ce didacticiel, je vais vous montrer quelques exemples concrets d'utilisation de PHP et IMAP pour créer de nouvelles fonctionnalités de gestion du courrier électronique que les grands fournisseurs de courrier électronique n'ont pas encore créées pour nous.
Mon intérêt pour cela a commencé en 2010 lorsque j'ai écrit Douze idées Gmail pour révolutionner (encore) le courrier électronique, mais la plupart des idées que je souhaitais étaient encore hors de portée. Malgré son importance, l’e-mail en tant qu’application a été assez lent à innover.
Nous sommes inondés d'e-mails et la gestion de notre boîte de réception peut encore être une lourde charge. Les services de courrier et les clients n’ont pas fait grand-chose pour nous aider à cet égard. La plupart des e-mails que nous recevons sont envoyés par des machines plutôt que par des personnes, mais nous devons tous les traiter individuellement.
L'analyse de mes propres e-mails a montré que j'avais reçu des e-mails de plus de 230 expéditeurs automatisés, alors que le nombre d'expéditeurs réels était bien inférieur. J'en ai marre de créer des filtres et de remplir d'innombrables formulaires de désabonnement dans Gmail. Je souhaite avoir plus de contrôle sur la gestion de mes emails et me simplifier la vie.
Enfin, au cours de la dernière année, j'ai décidé de créer les fonctionnalités dont j'avais besoin. Le résultat est Simplify Email (SE), une petite application Web que vous pouvez héberger vous-même et qui offre une variété de nouvelles fonctionnalités de messagerie intéressantes, que vous pouvez toutes consulter sur le site Web du projet.
Ce qui est intéressant avec SE, c'est qu'il s'agit d'une plate-forme de lecture, d'analyse, d'acheminement et de gestion des e-mails - les possibilités sont nombreuses. Le courrier électronique simplifié est essentiellement un terrain de jeu programmable pour « pirater » votre propre courrier électronique.
Je vais vous expliquer le code de trois exemples de traitement d'e-mails dans SE à l'aide de PHP, IMAP et MySQL :
Ce tutoriel vous donnera certainement une longueur d'avance sur l'écriture de code IMAP en PHP. Mais vous pouvez également utiliser directement la base de code Simplify Email. Vous pouvez acheter le code pour seulement 10 $, et il existe une ancienne version open source (à laquelle manque certaines des fonctionnalités décrites ci-dessous). Des instructions d'installation pour les configurations Linux typiques sont fournies. Je propose également des images préinstallées chez Digital Ocean pour 25 $ et propose une installation avec voiturier portable. SE est écrit en PHP dans le framework Yii.
Veuillez noter qu'à moins de compiler une bibliothèque IMAP sécurisée pour PHP, vous ne pourrez pas accéder à la plupart des serveurs de messagerie depuis votre machine de développement locale. C'est l'une des raisons pour lesquelles j'encourage les gens à exécuter Simplify Email sous forme de droplet dans Digital Ocean. Il existe également quelques conseils pour sécuriser votre compte Google lorsque vous vous connectez via IMAP.
Avec SE, vous pouvez continuer à utiliser le client de messagerie de votre choix sur le Web et les appareils mobiles. Vous n'avez pas besoin de modifier vos applications ou vos habitudes personnelles. SE accède à votre compte de messagerie en coulisse via IMAP ; en tant qu'assistant personnel intelligent, SE prétraite votre courrier électronique, déplaçant les messages vers l'emplacement approprié en fonction de tout ce que vous lui dites.
Lorsqu'un message arrive d'un expéditeur familier, SE le déplace vers le dossier que vous spécifiez. Lorsqu'un expéditeur inconnu reçoit un message pour la première fois, celui-ci est déplacé vers un dossier de révision.
Toutes les quelques heures (ou aussi souvent que vous le souhaitez), SE vous enverra un résumé de l'endroit où il a déplacé vos messages et des messages en cours d'examen. Notez que le dossier d'audit contient un lien vers l'expéditeur de la formation, ce qui facilite grandement la formation du SE au fil du temps.
Vous pouvez parcourir votre dossier de révision à tout moment - pas besoin d'attendre l'arrivée des résumés. Mais l’avantage de SE est que vous n’avez plus besoin de parcourir vos dossiers ; vous pouvez simplement lire le résumé pour visualiser les emails reçus et former les nouveaux expéditeurs.
SE utilise plusieurs tâches cron à exécuter en arrière-plan du serveur. Chacun est appelé depuis DaemonController.php
.
Le premier, processInbox
, est appelé fréquemment et doit être effectué rapidement : son travail consiste à passer au crible les e-mails et à les sortir de la boîte de réception et dans des dossiers catégorisés, appelés dossiers de filtrage, le plus rapidement possible.
Le second, processFiltering
, est plus gourmand en traitement et effectue des opérations plus approfondies sur l'e-mail, déplaçant finalement le message vers sa destination finale.
La tâche Cron est appelée régulièrement processInbox
:
public function actionInbox() { // moves inbox messages to @filtering // runs frequently $r = new Remote(); $r->processInbox(); }
Pour chaque compte, nous décryptons vos identifiants de messagerie, puis utilisons imap_open pour créer un flux IMAP dans votre dossier de boîte de réception :
public function open($account_id, $mailbox='',$options=NULL) { // opens folder in an IMAP account $account = Account::model()->findByPk($account_id); $this->hostname = $account->address; if (!stristr($this->hostname,'{')) $this->hostname = '{'.$this->hostname.'}'; $cred = Account::model()->getCredentials($account->cred); if ($account->provider == Account::PROVIDER_ICLOUD) { // icloud accepts only name part of mailbox e.g. stevejobs vs. stevejobs@icloud.com $temp = explode('@',$cred[0]); $cred[0]=$temp[0]; } $this->stream = imap_open($this->hostname.$mailbox,$cred[0],$cred[1],$options,1) or die('Cannot connect to mail server - account_id:'.$account_id .' '.print_r(imap_errors())); }
Dans processInbox
nous utilisons les fonctions de la bibliothèque PHP imap_search et imap_fetch_overview pour récupérer le tableau des messages :
// lookup folder_id of this account's INBOX $folder_id = Folder::model()->lookup($account_id,$this->path_inbox); $this->open($account_id,$this->path_inbox); $cnt=0; $message_limit= 50; // break after n messages to prevent timeout echo 'Sort since: '.date("j F Y",$tstamp); // imap_search date format 30 November 2013 $recent_messages = @imap_search($this->stream, 'SINCE "'.date("j F Y",$tstamp).'"',SE_UID); if ($recent_messages===false) continue; // to do - continue into next account $result = imap_fetch_overview($this->stream, implode(',',array_slice($recent_messages,0,$message_limit)),FT_UID);
Ensuite, nous traitons l'ensemble des messages dans la boîte de réception :
foreach ($result as $item) { if (!$this->checkExecutionTime($time_start)) break; // get msg header and stream uid $msg = $this->parseHeader($item);
Il s'agit d'une version adaptée du code d'analyse d'en-tête IMAP accessible au public qui collecte les informations supplémentaires nécessaires au SE pour accomplir diverses tâches. Fondamentalement, il utilise imap_rfc822_parse_adrlist pour déterminer les informations sur le destinataire, l'ID du message, l'objet et l'horodatage (ou les informations sur l'expéditeur lors de l'analyse du dossier Envoyé) :
public function parseHeader($header) { // parses header object returned from imap_fetch_overview if (!isset($header->from)) { return false; } else { $from_arr = imap_rfc822_parse_adrlist($header->from,'gmail.com'); $fi = $from_arr[0]; $msg = array( "uid" => (isset($header->uid)) ? $header->uid : 0, "personal" => (isset($fi->personal)) ? @imap_utf8($fi->personal) : "", "email" => (isset($fi->mailbox) && isset($fi->host)) ? $fi->mailbox . "@" . $fi->host : "", "mailbox" => (isset($fi->mailbox)) ? $fi->mailbox : "", "host" => (isset($fi->host)) ? $fi->host : "", "subject" => (isset($header->subject)) ? @imap_utf8($header->subject) : "", "message_id" => (isset($header->message_id)) ? $header->message_id : "", "in_reply_to" => (isset($header->in_reply_to)) ? $header->in_reply_to : "", "udate" => (isset($header->udate)) ? $header->udate : 0, "date_str" => (isset($header->date)) ? $header->date : "" ); // handles fetch with uid and rfc header parsing if ($msg['udate']==0 && isset($header->date)) { $msg['udate']=strtotime($header->date); } $msg['rx_email']=''; $msg['rx_personal']=''; $msg['rx_mailbox']=''; $msg['rx_host']=''; if (isset($header->to)) { $to_arr = imap_rfc822_parse_adrlist($header->to,'gmail.com'); $to_info = $to_arr[0]; if (isset($to_info->mailbox) && isset($to_info->host)) { $msg['rx_email']=$to_info->mailbox.'@'.$to_info->host; } if (isset($to_info->personal)) $msg['rx_personal']=$to_info->personal; if (isset($to_info->mailbox)) $msg['rx_mailbox']=$to_info->mailbox; if (isset($to_info->host)) $msg['rx_host']=$to_info->host; } return $msg; } }
Nous créons des enregistrements dans la base de données pour l'expéditeur et l'enveloppe du courrier :
// skip any system messages if ($msg['email']==$system_email) continue; // if udate is too old, skip msg if (time()-$msg['udate']>$this->scan_seconds) continue; // skip msg // default action $action = self::ACTION_MOVE_FILTERED; $isNew = $s->isNew($account_id,$msg["email"]); // look up sender, if new, create them $sender_id = $s->add($user_id,$account_id,$msg["personal"], $msg["mailbox"], $msg["host"],0); $sender = Sender::model()->findByPk($sender_id); // create a message in db if needed $message_id = $m->add($user_id,$account_id,0,$sender_id,$msg['message_id'],$msg['subject'],$msg['udate'],$msg['in_reply_to']); $message = Message::model()->findByPk($message_id);
Si l'expéditeur est nouveau (inconnu) pour nous, nous enverrons un e-mail de défi de liste blanche (nous discutons en détail des défis de liste blanche dans la section suivante ci-dessous) :
if ($isNew) { $this->challengeSender($user_id,$account_id,$sender,$message); }
Ensuite, nous déterminons si l'utilisateur a peut-être fait glisser un message d'un autre dossier vers la boîte de réception - dans l'intention de l'entraîner par glisser-déposer. Si tel est le cas, nous définirons la formation de cet expéditeur dans la boîte de réception. En d'autres termes, la prochaine fois, nous souhaitons acheminer uniquement le courrier de cet expéditeur vers la boîte de réception :
if ($message['status'] == Message::STATUS_FILTERED || $message['status'] == Message::STATUS_REVIEW || ($message['status'] == Message::STATUS_TRAINED && $message['folder_id'] <> $folder_id) || ($message['status'] == Message::STATUS_ROUTED && $message['folder_id'] <> $folder_id)) { // then it's a training $action = self::ACTION_TRAIN_INBOX; } else if (($message['status'] == Message::STATUS_TRAINED || $message['status'] == Message::STATUS_ROUTED) && $message['folder_id'] == $folder_id) { // if trained already or routed to inbox already, skip it $action = self::ACTION_SKIP; echo 'Trained previously, skip ';lb(); continue; }
Sinon, nous préparerons le message à déplacer vers le dossier Filtre pour un traitement ultérieur. Tout d'abord, nous pouvons envoyer une notification sur le téléphone de l'utilisateur si l'expéditeur de la notification correspond ou si un mot-clé correspond (et ce n'est pas une période calme) :
if ($action == self::ACTION_MOVE_FILTERED) { $cnt+=1; if ($sender->exclude_quiet_hours == Sender::EQH_YES or !$this->isQuietHours($user_id)) { // send smartphone notifications based on sender if ($sender->alert==Sender::ALERT_YES) { $this->notify($sender,$message,Monitor::NOTIFY_SENDER); } // send notifications based on keywords if (AlertKeyword::model()->scan($msg)) { $this->notify($sender,$message,Monitor::NOTIFY_KEYWORD); } } // move imap msg to +Filtering echo 'Moving to +Filtering';lb(); //$result = @imap_mail_move($this->stream,$msg['uid'],$this->path_filtering,CP_UID); $result = $this->messageMoveHandler($msg['uid'],$this->path_filtering,false); if ($result) { echo 'moved<br />'; $m->setStatus($message_id,Message::STATUS_FILTERED); } }
Si un message est glissé dans la boîte de réception, nous mettrons à jour nos paramètres d'entraînement :
else if ($action == self::ACTION_TRAIN_INBOX) { // set sender folder_id to inbox echo 'Train to Inbox';lb(); $m->setStatus($message_id,Message::STATUS_TRAINED); // only train sender when message is newer than last setting if ($msg['udate']>=$sender['last_trained']) { $s->setFolder($sender_id,$folder_id); } }
La méthode de traitement secondaire est appelée processFiltering
,也在DaemonController.php
中. Il s'agit du travail le plus fastidieux consistant à déplacer les messages vers les dossiers appropriés :
public function actionIndex() { // processes messages in @Filtering to appropriate folders $r = new Remote(); $r->processFiltering(); // Record timestamp of cronjob for monitoring $file = file_put_contents('./protected/runtime/cronstamp.txt',time(),FILE_USE_INCLUDE_PATH); }
Cette méthode ouvre votre compte de messagerie pour rechercher des messages récents et collecter des données à leur sujet. Il utilise également imap_search
、imap_fetch_overview
和 parseHeader
:
$tstamp = time()-(7*24*60*60); // 7 days ago $recent_messages = @imap_search($this->stream, 'SINCE "'.date("j F Y",$tstamp).'"',SE_UID); if ($recent_messages===false) continue; // to do - continue into next account $result = imap_fetch_overview($this->stream, implode(',',array_slice($recent_messages,0,$message_limit)),FT_UID); foreach ($result as $item) { $cnt+=1; if (!$this->checkExecutionTime($time_start)) break; // get msg header and stream uid $msg = $this->parseHeader($item);
La boucle de traitement principale pour chaque message du dossier filtre est très détaillée. Nous regardons d'abord l'adresse du destinataire, puisque SE permet de former des dossiers par adresse de destinataire, par exemple un message envoyé au domaine happyvegetarian.com ira dans le dossier veggie :
// Set the default action to move to the review folder $action = self::ACTION_MOVE_REVIEW; $destination_folder =0; // look up & create recipient $recipient_id = $r->add($user_id,$account_id,$msg['rx_email'],0); $routeByRx = $this->routeByRecipient($recipient_id); if ($routeByRx!==false) { $action = $routeByRx->action; $destination_folder = $routeByRx->destination_folder; }
Nous recherchons ensuite l'expéditeur et créons un nouvel enregistrement dans la base de données (si nécessaire). Si une formation existe pour l'expéditeur, nous pouvons définir le dossier cible :
// look up sender, if new, create them $sender_id = $s->add($user_id,$account_id,$msg["personal"], $msg["mailbox"], $msg["host"],0); $sender = Sender::model()->findByPk($sender_id); // if sender destination known, route to folder if ($destination_folder ==0 && $sender['folder_id'] > 0) { $action = self::ACTION_ROUTE_FOLDER; $destination_folder = $sender['folder_id']; }
Si le (nouvel) expéditeur non formé s'est authentifié via un défi de liste blanche (dont nous discutons dans la section suivante ci-dessous), alors nous acheminerons ce message vers la boîte de réception :
// whitelist verified senders go to inbox if ($sender->is_verified==1 && $sender['folder_id'] ==0 && UserSetting::model()->useWhitelisting($user_id)) { // place message in inbox $action = self::ACTION_ROUTE_FOLDER; $destination_folder = Folder::model()->lookup($account_id,$this->path_inbox); }
Nous créons ensuite une entrée de message dans la base de données qui contient les informations d'enveloppe concernant ce message :
// create a message in db $message = Message::model()->findByAttributes(array('message_id'=>$msg['message_id'])); if (!empty($message)) { // message exists already, $message_id = $message->id; } else { $message_id = $m->add($user_id,$account_id,0,$sender_id,$msg['message_id'],$msg['subject'],$msg['udate'],$msg['in_reply_to']); }
Si le message provient d'un expéditeur inconnu et non vérifié, nous pouvons déplacer le message vers un dossier de révision. Le dossier de révision contient tous les messages provenant d'expéditeurs que nous ne reconnaissons pas.
Si le message provient d'un expéditeur connu et que nous avons identifié la destination, nous pouvons le déplacer tant que ce n'est pas une heure calme (et que la fonction Ne pas déranger est désactivée) :
if ($recipient_id!==false) $m->setRecipient($message_id,$recipient_id); if ($action == self::ACTION_MOVE_REVIEW) { echo 'Moving to +Filtering/Review';lb(); //$result = @imap_mail_move($this->stream,$msg['uid'],$this->path_review,CP_UID); $result = $this->messageMoveHandler($msg['uid'],$this->path_review,false); if ($result) { echo 'moved<br />'; $m->setStatus($message_id,Message::STATUS_REVIEW); } } else if ($action == self::ACTION_ROUTE_FOLDER || $action == self::ACTION_ROUTE_FOLDER_BY_RX) { // lookup folder name by folder_id $folder = Folder::model()->findByPk($destination_folder); // if inbox & quiet hours, don't route right now if (strtolower($folder['name'])=='inbox' and $sender->exclude_quiet_hours == Sender::EQH_NO and $this->isQuietHours($user_id)) continue; echo 'Moving to '.$folder['name'];lb(); $mark_read = Folder::model()->isMarkRead($folder['mark_read']) || Sender::model()->isMarkRead($sender['mark_read']); //$result = @imap_mail_move($this->stream,$msg['uid'],$folder['name'],CP_UID); $result = $this->messageMoveHandler($msg['uid'],$folder['name'],$mark_read); if ($result) { echo 'moved<br />'; $m->setStatus($message_id,Message::STATUS_ROUTED); $m->setFolder($message_id,$destination_folder); } }
Pendant les périodes calmes, les messages sont principalement enregistrés dans des dossiers filtrés.
Toutes les quelques heures, un processus différent crée un résumé des messages à l'aide des enregistrements de la table des messages pour déterminer les e-mails récemment reçus et filtrés ainsi que la manière dont ils ont été acheminés.
L'objectif du défi de la liste blanche est de conserver tous les messages provenant d'expéditeurs inconnus, tels que des robots marketing ou des spammeurs, qui pourraient se trouver dans votre boîte de réception. SE place les messages provenant d'expéditeurs inconnus dans un dossier de révision. Cependant, si vous activez la liste blanche, nous enverrons un e-mail de défi pour donner à l'expéditeur la possibilité de vérifier qu'il est humain. S'ils répondent, nous déplacerons le message vers votre boîte de réception. Si l'e-mail s'avère indésirable, vous pouvez supprimer le message du résumé ou le faire glisser vers n'importe quel dossier dans lequel vous souhaitez l'entraîner.
Les utilisateurs peuvent activer et désactiver la liste blanche dans les paramètres :
Pour mettre en œuvre la liste blanche, nous enverrons un défi par e-mail chaque fois qu'un nouvel expéditeur recevra un message :
if ($isNew) { $this->challengeSender($user_id,$account_id,$sender,$message); }
ChallengeSender
Envoyez aux utilisateurs un lien codé sur lequel ils pourront cliquer. Nous avons également mis en place certaines mesures de protection pour garantir que nous ne restons pas coincés dans une boucle de courrier électronique avec des messages d'absence du bureau :
public function challengeSender($user_id,$account_id,$sender,$message) { // whitelist email challenge $yg = new Yiigun(); $ac = Account::model()->findByPk($account_id); if (!empty($ac['challenge_name'])) $from = $ac['challenge_name'].' <no-reply@'.$yg->mg_domain.'>'; else $from = 'Filter <no-reply@'.$yg->mg_domain.'>'; $cred = Account::model()->getCredentials($ac->cred); $account_email = $cred[0]; unset($cred); // safety: checks no recent email if ($sender->last_emailed>(time()-(48*60*60))) return false; if ($sender->isBot($sender['email'])) { // to do - can also set this person to bulk by default return false; } $link=Yii::app()->getBaseUrl(true)."/sender/verify/s/".$sender->id."/m/".$message->id.'/u/'.$message->udate; $subject = 'Please verify the message you sent to '.$account_email; $body="<p>Hi,<br /><br /> I'm trying to reduce unsolicited email. Could you please verify your email address by clicking the link below:<br /><a href=\"".$link.'">'.$link.'</a><br /><br />Verifying your email address will help speed your message into my inbox. Thanks for your assistance!</p>'; $yg->send_html_message($from, $sender['email'], $subject,$body); // update last_emailed $sender->touchLastEmailed($sender->id); }
Ensuite, si le destinataire clique sur le lien encodé, nous le vérifions dans la base de données. Le contrôleur émetteur traite ces demandes et vérifie leur validité :
public function actionVerify($s = 0, $m=0,$u=0) { // verify that secure msg url from digest is valid, log in user, show msg $sender_id = $s; $message_id = $m; $udate = $u; $msg = Message::model()->findByPk($message_id); if (!empty($msg) && $msg->sender_id == $sender_id && $msg->udate == $udate) { $result = 'Thank you for your assistance. I\'ll respond to your email as soon as possible.'; $a = new Advanced(); $a->verifySender($msg->account_id,$sender_id); } else { $result = 'Sorry, we could not verify your email address.'; } $this->render('verify',array( 'result'=>$result, )); }
Cela indique à notre boucle de traitement de déplacer ce message et les futurs messages de cet expéditeur vers la boîte de réception.
有时,查看您已发送但未收到回复的消息摘要会有所帮助。为了识别这些邮件,Simplify Email 会监视已发送但尚未收到回复的邮件。
我们收到的每条消息都包含一个唯一的 ID,称为 message_id(IMAP 规范的一部分)。它通常看起来像这样:
Message-Id: <CALe0OAaF3fb3d=gCq2Fs=Ex61Qp6FdbiA4Mvs6kTQ@mail.gmail.com>
此外,当发送消息以回复其他消息时,它们有一个 in_reply_to
字段,该字段链接回原始 message_id
。
因此,我们使用 SQL 查询来查找所有收到的消息,这些消息没有引用其 message_id
的相应回复消息。为此,我们在没有 in_reply_to
id 的情况下使用 LEFT OUTER JOIN:
public function getUnanswered($account_id,$mode=0, $range_days = 7) { if ($mode==0) $subject_compare = 'not'; else $subject_compare = ''; $query = Yii::app()->db->createCommand("SELECT fi_sent_message.id, fi_sent_message.recipient_id as sender_id,fi_sent_message.subject,fi_sent_message.udate,fi_message.in_reply_to,fi_sent_message.message_id FROM fi_sent_message LEFT OUTER JOIN fi_message ON fi_message.in_reply_to = fi_sent_message.message_id WHERE fi_sent_message.account_id = ".$account_id." AND fi_message.in_reply_to is null and fi_sent_message.udate > ".(time()-(3600*24*$range_days))." and fi_sent_message.subject ".$subject_compare." like 'Re: %' ORDER BY fi_sent_message.udate DESC")->queryAll(); return $query; }
我们使用 $subject_compare
模式来区分我们发送的尚未回复的消息和我们发送给尚未回复的线程的回复。以下是您帐户中的未回复消息报告:
SE 还将此信息作为可选摘要提供,称为未回复电子邮件摘要。您可以每天、每隔几天或每周收到它。
我们还使用类似的 SQL 表格和 Google Charts 来提供有关某些人向您发送电子邮件的频率的报告:
public function reportInbound($account_id,$range=30,$limit = 100) { $result= Yii::app()->db->createCommand('SELECT fi_sender.personal, fi_sender.email,count(sender_id) as cnt FROM fi_message LEFT JOIN fi_sender ON fi_sender.id =fi_message.sender_id WHERE fi_sender.account_id = :account_id AND fi_message.created_at > DATE_SUB( NOW() , INTERVAL :range DAY ) GROUP BY sender_id ORDER BY cnt desc LIMIT :limit ')->bindValue('range',$range)->bindValue('account_id',$account_id)->bindValue('limit',$limit)->queryAll(); return $result; }
我很快就会撰写更多有关 Tuts+ 的 Google Charts 的文章。
我希望您已经发现 Simplify Email 足够有趣,可以尝试 PHP IMAP 编程。您可以构建许多很酷的功能,而不需要大型电子邮件提供商做任何新的事情。
如果您有任何疑问或更正,请在评论中提出。如果您想继续关注我未来的 Tuts+ 教程和其他系列,请关注 @reifman 或访问我的作者页面。您也可以在这里联系我。
以下是一些您可能会觉得有用的附加链接:
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!