Κυλλήνη

Перекидывание почты в базу: PHP, IMAP, MySQL

Возникла необходимость перелить в базу данных содержимое почтового ящика. По получившемуся архиву должен осуществляться относительно быстрый поиск. Следующий скрипт забирает письма по IMAP, парсит их, помещает в MySQL, и при необходимости удаляет обработанную почту (отвечающий за удаление кусок кода нужно раскомментировать). Если удалять письма не нужно, то для более быстрой работы можно использовать опцию readonly при инициализации соединения с почтовым сервером.

В скрипте не предусмотрена обработка писем во вложенных папках, так как это не требовалось. Данные собираются в поля: дата, отправитель, получатели (to, cc и bcc скомбинированы в одну строку) и тема письма. Полная копия письма также сохраняется в BLOB.

Код писался на скорую руку и при определённых условиях фэйлит, но задачу свою он в итоге с нескольких попыток всё же выполнил. Так что отсюда можно выдрать несколько полезных моментов, в частности, обработку темы письма.

В случае остановки скрипта на произвольном месте, можно посмотреть в логе предыдущий обработанный UID письма и передать его в качестве параметра при следующем запуске, при этом обработка почты продолжится с указанного места.

#!/usr/bin/php -f
< ?php
	echo "Running...\n";
    
	/* login setup */
    
    //	$imap_hostname = '{mail.example.com:993/imap/ssl/readonly}INBOX';
    //	$imap_username = 'user@example.com';
    //	$imap_password = 'password';
    
    //	$imap_hostname = '{mail.example.com:143/debug/imap2/notls/readonly}INBOX';
    //	$imap_hostname = '{mail.example.com:993/debug/imap/tls/novalidate-cert/readonly}INBOX';
	
    $imap_hostname = '{mail.example.com:143/imap4/notls}INBOX';
	$imap_username = 'user';
	$imap_password = 'password';
	
	$db_hostname = 'localhost';
	$db_username = 'dbuser';
	$db_password = 'dbpassword';
	$db_base = 'dbname';
    
    // counter of total messages to be processed
	$messages_to_process = 0;
	
    // if something goes wrong, this script may be re-run with id of last processed message
	$tocontinue = 0;
	if(isset($argv[1]))
    {
        $tocontinue = $argv[1];
    }
    
	/* try to connect */
	imap_timeout(IMAP_READTIMEOUT, 360);
	imap_timeout(IMAP_OPENTIMEOUT, 360);
    
	$inbox = imap_open($imap_hostname, $imap_username, $imap_password, OP_SHORTCACHE) or die('Cannot connect to imap: ' . imap_last_error());
	
    // remove messages marked for deletion, may be slow:
    imap_expunge($inbox);
	
	$dblink = mysqli_connect($db_hostname, $db_username, $db_password, $db_base);
	if (mysqli_connect_errno()) {
		printf("MySQL connect failed: %s\n", mysqli_connect_error());
		exit();
	}
    
	/* change dblink character set to utf8 */
	if (!mysqli_set_charset($dblink, "utf8"))
	{
		printf("Error loading character set utf8: %s\n", mysqli_error($dblink));
	}
    else
	{
		printf("Current character set: %s\n", mysqli_character_set_name($dblink));
	}
    
	echo "Connected!\n";
    
    // messages in imap folder
	$num_mgs = imap_num_msg($inbox);
	$messages_to_process = $num_mgs;
	echo "$num_mgs messages to read\n";
    
	/* grab emails */
	$emails = imap_search($inbox,'ALL', SE_UID);
	if($emails === false)
	{
		die('Could not perform imap search: '. imap_last_error());
	}
    
    // if argument is supplied, continue from requested point
	if($tocontinue != 0)
	{
		$tocontinue = array_search($tocontinue, $emails);
		$messages_to_process = $num_mgs - $tocontinue;
	}
    
	/* if emails are returned, cycle through each */
    
    /* for every email... */
    for($curid = $tocontinue; $curid < sizeof($emails); $curid++)
    {
        // dirty code. does garbage cleaning every 15 iterations, otherwise script consumes HUGE amounts of memory
        if(version_compare(PHP_VERSION, '5.3.0', '>=') && $curid % 15 == 0)
        {
            imap_gc($inbox, IMAP_GC_ELT);
        }
        
        $email_number = $emails[$curid];
        
        // check every 90 iterations if imap is still alive, may be slow
        if($curid % 90 == 0 && !imap_ping ($inbox))
        {
            die("Imap is dead");
        }
        
        // $email_number = $messages_to_process;
        $messages_to_process--;
        
        print "-------------------------------------------------\n";
        
        /* get information specific to this email */
        
        //$overview = imap_fetch_overview($inbox,$email_number,0);
        //$struct = imap_fetchstructure($inbox,$email_number);
        
        $rheaders = imap_fetchheader($inbox, $email_number, FT_UID);
        $pheaders = imap_rfc822_parse_headers($rheaders);
        
        // FIXME: wtf?!
        if(empty($pheaders->sender) && empty($pheaders->to))
        {
            echo "rh: "; print_r($rheaders); echo "\n";
            echo "ph: "; print_r($pheaders); echo "\n";
            echo "ib: "; print_r($inbox); echo "\n";
            printf("FAILURE: %d!\n", $email_number);
            continue;
        }
        
        printf("UID being processed: %s\n", $email_number);
        
        /* getting email subject, some dirty code, may be slow */
        $ft_subj = decodeHeader(imap_utf8($pheaders->subject));
        //$ft_subj = imap_utf8($pheaders->subject);
        
        print 'subj: ' . $ft_subj . "\n";
        
        /* getting email date */
        $ft_date = $pheaders->date;
        print 'date: ' . $ft_date . "\n";
        
        /* getting sender and recipients */
        if (!is_array($pheaders->sender) || count($pheaders->sender) < 1)
        {
            print_r($pheaders);
            printf("\n");
            die("Something is wrong with SENDER address\n");
        }
        
        $ft_sender = $pheaders->sender[0]->mailbox . "@" . $pheaders->sender[0]->host;
        print ' sender: ' . $ft_sender . "\n";
        
        // collect message recepients data (to, cc, bcc)
        $receivers = array();
        if (!is_array($pheaders->to) || count($pheaders->to) < 1)
        {
            print_r($pheaders);
            printf("\n");
            // die("Something is totally wrong with TO addresses\n"); // may happen when there's only CC address
        }
        if(is_array($pheaders->to))
        {
            $receivers = $pheaders->to;
        }
        
        if (isset($pheaders->cc) && is_array($pheaders->cc) && count($pheaders->cc) > 0)
        {
            $receivers = array_merge($receivers, $pheaders->cc);
        }
        if (isset($pheaders->bcc) && is_array($pheaders->bcc) && count($pheaders->bcc) > 0)
        {
            $receivers = array_merge($receivers, $pheaders->bcc);
        }
        
        foreach ($receivers as $id => $val)
        {
            // echo " to: " . $val->mailbox . "@" . $val->host . "\n";
            $receivers[$id] = $val->mailbox . "@" . $val->host;
        }
        
        /* make comma-separated list of recipients */
        $ft_receivers = implode(', ', $receivers);
        print "to: " . $ft_receivers . "\n";
        
        /* email body */
        $ft_data = $rheaders . imap_body($inbox,$email_number, FT_UID | FT_PEEK | FT_INTERNAL);
        
        $ft_date_converted = date("Y-m-d g:i:s a", strtotime($ft_date));
        
        /* inserting all collected values to database */
        $query = "INSERT into `mailarch_def` (`from`, `to`, `subj`, `date`, `data`) VALUES (?,?,?,?,?)";
        $stmt = mysqli_prepare($dblink, $query);
        mysqli_stmt_bind_param($stmt, "sssss",
                               $ft_sender,
                               $ft_receivers,
                               $ft_subj,
                               $ft_date_converted,
                               $ft_data
                               );
        
        
        /* execute prepared statement */
        mysqli_stmt_execute($stmt);
        
        /* check result. in this case error is triggered mostly by unique keys in db. it is ok */
        $aff_rows = mysqli_stmt_affected_rows($stmt);
        ($aff_rows > 0) ? printf("%d Row inserted\n", $aff_rows) : printf("Duplicate or error.\n". mysqli_error($dblink)."\n");
        
        printf("%d Messages to process\n", $messages_to_process);
        
        /* close statement and connection */
        mysqli_stmt_close($stmt);
        
        /* WARNING! to remove processed messages uncomment the following line */
        //imap_delete($inbox, $email_number, FT_UID);
        
        /* this was a test, it should stay commented */
        //imap_setflag_full($inbox, $email_number, '\Deleted', FT_UID);
    }
    
	/* close connections */
	imap_close($inbox);
	mysqli_close($dblink);
    
	exit('Done!');
    
    /* workaround to make most of headers to parse properly */
	function decodeHeader($hdr, $cset = 'UTF8')
	{
		// Copied nearly intact from PEAR's Mail_mimeDecode.
		$hdr = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $hdr);
		$m = array();
		if(is_array($hdr))
			$hdr = $hdr[0];
		while(preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $hdr, $m))
		{
			$encoded  = $m[1];
			$charset  = strtoupper($m[2]);
			$encoding = strtolower($m[3]);
			$text     = $m[4];
            
			switch($encoding)
			{
				case 'b':
					$text = base64_decode($text);
					break;
				case 'q':
					$text = str_replace('_', ' ', $text);
					preg_match_all('/=([a-f0-9]{2})/i', $text, $m);
					foreach($m[1] as $value)
                    $text = str_replace('=' . $value, chr(hexdec($value)), $text);
					break;
			}
			if($charset !== $cset)
				$text = charconv($charset, $cset, $text);
			$hdr = str_replace($encoded, $text, $hdr);
		}
		return $hdr;
	}
    
    /* workaround to make most of headers to parse properly */
	function charconv($enc_from, $enc_to, $text)
	{
		if(function_exists('iconv'))
			return iconv($enc_from, $enc_to, $text);
		elseif(function_exists('recode_string'))
        return recode_string("$enc_from..$enc_to", $text);
		elseif(function_exists('mb_convert_encoding'))
        return mb_convert_encoding($text, $enc_to, $enc_from);
		return $text;
	}
    
    ?>